summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md7
-rw-r--r--Makefile17
-rw-r--r--api/admin.go2
-rw-r--r--api/api.go1
-rw-r--r--api/file.go4
-rw-r--r--api/preference.go121
-rw-r--r--api/preference_test.go201
-rw-r--r--api/templates/email_change_verify_body.html2
-rw-r--r--config/config.json6
-rw-r--r--doc/README.md4
-rw-r--r--doc/api/Overview.md2
-rw-r--r--doc/developer/Code-Contribution-Guidelines.md10
-rw-r--r--doc/developer/Setup.md4
-rw-r--r--doc/help/Manage-Members.md (renamed from doc/help/Manage-Team.md)8
-rw-r--r--doc/help/README.md2
-rw-r--r--doc/help/Slack-Import.md16
-rw-r--r--doc/install/Configuration-Settings.md302
-rw-r--r--doc/install/Release-Numbering.md2
-rw-r--r--doc/install/SMTP-Email-Setup.md14
-rw-r--r--doc/install/Troubleshooting.md7
-rw-r--r--doc/integrations/Single-Sign-On/Gitlab.md12
-rw-r--r--doc/integrations/webhooks/Incoming-Webhooks.md68
-rw-r--r--docker/0.7/Dockerrun.aws.zipbin867 -> 0 bytes
-rw-r--r--docker/0.7/config_docker.json103
-rw-r--r--docker/1.1/Dockerfile (renamed from docker/0.7/Dockerfile)2
-rw-r--r--docker/1.1/Dockerrun.aws.zipbin0 -> 710 bytes
-rw-r--r--docker/1.1/Dockerrun.aws/.ebextensions/01_files.config (renamed from docker/0.7/Dockerrun.aws/.ebextensions/01_files.config)0
-rwxr-xr-xdocker/1.1/Dockerrun.aws/Dockerrun.aws.json (renamed from docker/0.7/Dockerrun.aws/Dockerrun.aws.json)2
-rw-r--r--docker/1.1/config_docker.json92
-rwxr-xr-xdocker/1.1/docker-entry.sh (renamed from docker/0.7/docker-entry.sh)0
-rw-r--r--docker/dev/config_docker.json6
-rw-r--r--docker/local/config_docker.json6
-rw-r--r--mattermost.go38
-rw-r--r--model/client.go26
-rw-r--r--model/config.go13
-rw-r--r--model/preference.go60
-rw-r--r--model/preference_test.go56
-rw-r--r--model/preferences.go31
-rw-r--r--model/version.go1
-rw-r--r--store/sql_preference_store.go214
-rw-r--r--store/sql_preference_store_test.go146
-rw-r--r--store/sql_store.go30
-rw-r--r--store/sql_user_store.go21
-rw-r--r--store/sql_user_store_test.go20
-rw-r--r--store/store.go8
-rw-r--r--utils/config.go2
-rw-r--r--utils/diagnostic.go19
-rw-r--r--web/react/components/admin_console/gitlab_settings.jsx16
-rw-r--r--web/react/components/admin_console/privacy_settings.jsx34
-rw-r--r--web/react/components/admin_console/service_settings.jsx37
-rw-r--r--web/react/components/admin_console/user_item.jsx6
-rw-r--r--web/react/components/channel_loader.jsx6
-rw-r--r--web/react/components/create_comment.jsx40
-rw-r--r--web/react/components/email_verify.jsx2
-rw-r--r--web/react/components/find_team.jsx1
-rw-r--r--web/react/components/get_link_modal.jsx1
-rw-r--r--web/react/components/invite_member_modal.jsx3
-rw-r--r--web/react/components/login.jsx2
-rw-r--r--web/react/components/member_list_team_item.jsx6
-rw-r--r--web/react/components/more_direct_channels.jsx59
-rw-r--r--web/react/components/navbar_dropdown.jsx2
-rw-r--r--web/react/components/password_reset_form.jsx1
-rw-r--r--web/react/components/password_reset_send_link.jsx1
-rw-r--r--web/react/components/post_info.jsx1
-rw-r--r--web/react/components/setting_item_max.jsx11
-rw-r--r--web/react/components/settings_sidebar.jsx2
-rw-r--r--web/react/components/sidebar.jsx260
-rw-r--r--web/react/components/sidebar_right_menu.jsx2
-rw-r--r--web/react/components/signup_user_complete.jsx5
-rw-r--r--web/react/components/team_import_tab.jsx6
-rw-r--r--web/react/components/team_signup_display_name_page.jsx1
-rw-r--r--web/react/components/team_signup_email_item.jsx1
-rw-r--r--web/react/components/team_signup_password_page.jsx1
-rw-r--r--web/react/components/team_signup_url_page.jsx1
-rw-r--r--web/react/components/team_signup_username_page.jsx1
-rw-r--r--web/react/components/team_signup_welcome_page.jsx1
-rw-r--r--web/react/components/team_signup_with_email.jsx1
-rw-r--r--web/react/components/team_signup_with_sso.jsx1
-rw-r--r--web/react/components/textbox.jsx2
-rw-r--r--web/react/components/user_settings/manage_incoming_hooks.jsx2
-rw-r--r--web/react/components/user_settings/user_settings_appearance.jsx2
-rw-r--r--web/react/components/user_settings/user_settings_general.jsx3
-rw-r--r--web/react/components/user_settings/user_settings_integrations.jsx4
-rw-r--r--web/react/components/view_image.jsx3
-rw-r--r--web/react/stores/preference_store.jsx122
-rw-r--r--web/react/utils/async_client.jsx52
-rw-r--r--web/react/utils/client.jsx28
-rw-r--r--web/react/utils/constants.jsx8
-rw-r--r--web/react/utils/utils.jsx8
-rw-r--r--web/sass-files/sass/partials/_admin-console.scss3
-rw-r--r--web/sass-files/sass/partials/_base.scss5
-rw-r--r--web/sass-files/sass/partials/_content.scss1
-rw-r--r--web/sass-files/sass/partials/_headers.scss5
-rw-r--r--web/sass-files/sass/partials/_markdown.scss9
-rw-r--r--web/sass-files/sass/partials/_modal.scss3
-rw-r--r--web/sass-files/sass/partials/_navbar.scss6
-rw-r--r--web/sass-files/sass/partials/_post.scss35
-rw-r--r--web/sass-files/sass/partials/_responsive.scss14
-rw-r--r--web/sass-files/sass/partials/_settings.scss1
-rw-r--r--web/sass-files/sass/partials/_sidebar--left.scss3
-rw-r--r--web/sass-files/sass/partials/_sidebar--right.scss1
-rw-r--r--web/templates/channel.html2
-rw-r--r--web/templates/head.html2
103 files changed, 2072 insertions, 472 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 66e430f7c..eac1e61e5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,9 +2,9 @@
## UNDER DEVELOPMENT - Release v1.1.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
+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
### Release Highlights
@@ -20,7 +20,6 @@ Integrations
Documentation
-- Contents of `/docs` now hosted at docs.mattermost.org
- Added documentation on config.json and System Console settings
- Docker Toolbox replaces deprecated Boot2Docker instructions in container install documentation
diff --git a/Makefile b/Makefile
index b03ed5cba..103370288 100644
--- a/Makefile
+++ b/Makefile
@@ -88,6 +88,11 @@ travis:
mkdir -p $(DIST_PATH)/web/static/js
cp -L web/static/js/*.min.js $(DIST_PATH)/web/static/js/
+ cp -RL web/static/config $(DIST_PATH)/web/static
+ cp -RL web/static/css $(DIST_PATH)/web/static
+ cp -RL web/static/fonts $(DIST_PATH)/web/static
+ cp -RL web/static/help $(DIST_PATH)/web/static
+ cp -RL web/static/images $(DIST_PATH)/web/static
cp -RL web/static/js/jquery-dragster $(DIST_PATH)/web/static/js/
cp -RL web/templates $(DIST_PATH)/web
@@ -111,7 +116,7 @@ travis:
rm $(DIST_PATH)/web/templates/*.bak
mv doc/README.md doc/index.md
- mkdocs build
+ mkdocs build --strict
cp -r documentation-html $(DIST_PATH)/documentation-html
tar -C dist -czf $(DIST_PATH).tar.gz mattermost
@@ -265,8 +270,14 @@ dist: install
cd web/sass-files && compass compile -e production --force
- mkdir -p $(DIST_PATH)/web
- cp -RL web/static $(DIST_PATH)/web
+ mkdir -p $(DIST_PATH)/web/static/js
+ cp -L web/static/js/*.min.js $(DIST_PATH)/web/static/js/
+ cp -RL web/static/config $(DIST_PATH)/web/static
+ cp -RL web/static/css $(DIST_PATH)/web/static
+ cp -RL web/static/fonts $(DIST_PATH)/web/static
+ cp -RL web/static/help $(DIST_PATH)/web/static
+ cp -RL web/static/images $(DIST_PATH)/web/static
+ cp -RL web/static/js/jquery-dragster $(DIST_PATH)/web/static/js/
cp -RL web/templates $(DIST_PATH)/web
mkdir -p $(DIST_PATH)/api
diff --git a/api/admin.go b/api/admin.go
index 2167868e0..cd1e5d2de 100644
--- a/api/admin.go
+++ b/api/admin.go
@@ -104,6 +104,8 @@ func saveConfig(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
+ cfg.SetDefaults()
+
if err := cfg.IsValid(); err != nil {
c.Err = err
return
diff --git a/api/api.go b/api/api.go
index 5c3c0d8c6..4da1de62d 100644
--- a/api/api.go
+++ b/api/api.go
@@ -45,6 +45,7 @@ func InitApi() {
InitAdmin(r)
InitOAuth(r)
InitWebhook(r)
+ InitPreference(r)
templatesDir := utils.FindDir("api/templates")
l4g.Debug("Parsing server templates at %v", templatesDir)
diff --git a/api/file.go b/api/file.go
index 9ebcd821b..429347596 100644
--- a/api/file.go
+++ b/api/file.go
@@ -408,11 +408,11 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Length", strconv.Itoa(len(f)))
w.Header().Del("Content-Type") // Content-Type will be set automatically by the http writer
- // attach extra headers to trigger a download on IE and Edge
+ // attach extra headers to trigger a download on IE, Edge, and Safari
ua := user_agent.New(r.UserAgent())
bname, _ := ua.Browser()
- if bname == "Edge" || bname == "Internet Explorer" {
+ if bname == "Edge" || bname == "Internet Explorer" || bname == "Safari" {
// trim off anything before the final / so we just get the file's name
parts := strings.Split(filename, "/")
diff --git a/api/preference.go b/api/preference.go
new file mode 100644
index 000000000..88cb132f8
--- /dev/null
+++ b/api/preference.go
@@ -0,0 +1,121 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ l4g "code.google.com/p/log4go"
+ "github.com/gorilla/mux"
+ "github.com/mattermost/platform/model"
+ "net/http"
+)
+
+func InitPreference(r *mux.Router) {
+ l4g.Debug("Initializing preference api routes")
+
+ sr := r.PathPrefix("/preferences").Subrouter()
+ 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 savePreferences(c *Context, w http.ResponseWriter, r *http.Request) {
+ preferences, err := model.PreferencesFromJson(r.Body)
+ if err != nil {
+ c.Err = model.NewAppError("savePreferences", "Unable to decode preferences from request", err.Error())
+ c.Err.StatusCode = http.StatusBadRequest
+ return
+ }
+
+ for _, preference := range preferences {
+ if c.Session.UserId != preference.UserId {
+ c.Err = model.NewAppError("savePreferences", "Unable to set preferences for other user", "session.user_id="+c.Session.UserId+", preference.user_id="+preference.UserId)
+ c.Err.StatusCode = http.StatusUnauthorized
+ return
+ }
+ }
+
+ if result := <-Srv.Store.Preference().Save(&preferences); result.Err != nil {
+ c.Err = result.Err
+ return
+ }
+
+ w.Write([]byte("true"))
+}
+
+func getPreferenceCategory(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+ category := params["category"]
+
+ if result := <-Srv.Store.Preference().GetCategory(c.Session.UserId, category); result.Err != nil {
+ c.Err = result.Err
+ } 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"]
+ name := params["name"]
+
+ if result := <-Srv.Store.Preference().Get(c.Session.UserId, category, name); result.Err != nil {
+ c.Err = result.Err
+ } else {
+ data := result.Data.(model.Preference)
+ w.Write([]byte(data.ToJson()))
+ }
+}
diff --git a/api/preference_test.go b/api/preference_test.go
new file mode 100644
index 000000000..318ce9582
--- /dev/null
+++ b/api/preference_test.go
@@ -0,0 +1,201 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/store"
+ "testing"
+)
+
+func TestSetPreferences(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))
+
+ Client.LoginByEmail(team.Name, user1.Email, "pwd")
+
+ // save 10 preferences
+ var preferences model.Preferences
+ for i := 0; i < 10; i++ {
+ preference := model.Preference{
+ UserId: user1.Id,
+ Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
+ Name: model.NewId(),
+ }
+ preferences = append(preferences, preference)
+ }
+
+ if _, err := Client.SetPreferences(&preferences); err != nil {
+ t.Fatal(err)
+ }
+
+ // update 10 preferences
+ for _, preference := range preferences {
+ preference.Value = "1234garbage"
+ }
+
+ if _, err := Client.SetPreferences(&preferences); err != nil {
+ t.Fatal(err)
+ }
+
+ // not able to update as a different user
+ 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))
+
+ Client.LoginByEmail(team.Name, user2.Email, "pwd")
+
+ if _, err := Client.SetPreferences(&preferences); err == nil {
+ t.Fatal("shouldn't have been able to update another user's preferences")
+ }
+}
+
+func TestGetPreferenceCategory(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))
+
+ Client.LoginByEmail(team.Name, user1.Email, "pwd")
+
+ if result, err := Client.GetPreferenceCategory(category); err != nil {
+ t.Fatal(err)
+ } else if data := result.Data.(model.Preferences); len(data) != 2 {
+ 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])) {
+ t.Fatal("received incorrect preferences")
+ }
+
+ Client.LoginByEmail(team.Name, user2.Email, "pwd")
+
+ if result, err := Client.GetPreferenceCategory(category); err != nil {
+ t.Fatal(err)
+ } else if data := result.Data.(model.Preferences); len(data) != 0 {
+ t.Fatal("received the wrong number of preferences")
+ }
+}
+
+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()
+
+ 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)
+
+ user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user.Id))
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ preferences := model.Preferences{
+ {
+ UserId: user.Id,
+ Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
+ Name: model.NewId(),
+ Value: model.NewId(),
+ },
+ }
+
+ Client.Must(Client.SetPreferences(&preferences))
+
+ if result, err := Client.GetPreference(preferences[0].Category, preferences[0].Name); err != nil {
+ t.Fatal(err)
+ } else if data := result.Data.(*model.Preference); *data != preferences[0] {
+ t.Fatal("preference saved incorrectly")
+ }
+
+ preferences[0].Value = model.NewId()
+ Client.Must(Client.SetPreferences(&preferences))
+
+ if result, err := Client.GetPreference(preferences[0].Category, preferences[0].Name); err != nil {
+ t.Fatal(err)
+ } else if data := result.Data.(*model.Preference); *data != preferences[0] {
+ t.Fatal("preference updated incorrectly")
+ }
+}
diff --git a/api/templates/email_change_verify_body.html b/api/templates/email_change_verify_body.html
index 1e1bcc22d..a9b2a0741 100644
--- a/api/templates/email_change_verify_body.html
+++ b/api/templates/email_change_verify_body.html
@@ -9,7 +9,7 @@
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="padding: 20px 20px 10px; text-align:left;">
- <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt="">
+ <img src="{{.Props.SiteURL}}/static/images/logo-email.png" width="130px" style="opacity: 0.5" alt="">
</td>
</tr>
<tr>
diff --git a/config/config.json b/config/config.json
index 919737da7..8ef151350 100644
--- a/config/config.json
+++ b/config/config.json
@@ -8,7 +8,8 @@
"EnableIncomingWebhooks": true,
"EnablePostUsernameOverride": false,
"EnablePostIconOverride": false,
- "EnableTesting": false
+ "EnableTesting": false,
+ "EnableSecurityFixAlert": true
},
"TeamSettings": {
"SiteName": "Mattermost",
@@ -77,8 +78,7 @@
},
"PrivacySettings": {
"ShowEmailAddress": true,
- "ShowFullName": true,
- "EnableSecurityFixAlert": true
+ "ShowFullName": true
},
"GitLabSettings": {
"Enable": false,
diff --git a/doc/README.md b/doc/README.md
index f106cdfc8..7ee2bb46c 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -26,14 +26,14 @@ Set up Mattermost in your data center
- [Developer Machine Setup](developer/Setup.md)
- [Mattermost Style Guide](developer/Style-Guide.md)
- [API Overview](api/Overview.md)
- - [Incoming Webhooks](integrations/webhooks/Incoming.md)
+ - [Incoming Webhooks](integrations/webhooks/Incoming-Webhooks.md)
## Help
_Note: End user help documentation is a new feature being completed for the v1.2 release. The materials below are work in progress._
- User Interface
- - [Manage Team](help/Manage-Team.md)
+ - [Manage Members](help/Manage-Members.md)
- Team Settings
- [Slack Import](help/Slack-Import.md)
diff --git a/doc/api/Overview.md b/doc/api/Overview.md
index 02e11974e..0a407b1e9 100644
--- a/doc/api/Overview.md
+++ b/doc/api/Overview.md
@@ -77,7 +77,7 @@ X-Ratelimit-Reset: 1441983590
These headers are telling you your current rate limit status.
Header | Description
---------------------- | -----------
+:--------------------- |:-----------
X-Ratelimit-Limit | The maximum number of requests you can make per second.
X-Ratelimit-Remaining | The number of requests remaining in the current window.
X-Ratelimit-Reset | The remaining UTC epoch seconds before the rate limit resets.
diff --git a/doc/developer/Code-Contribution-Guidelines.md b/doc/developer/Code-Contribution-Guidelines.md
index 80676f107..48bbf2491 100644
--- a/doc/developer/Code-Contribution-Guidelines.md
+++ b/doc/developer/Code-Contribution-Guidelines.md
@@ -5,7 +5,6 @@ Thank you for your interest in contributing to Mattermost. This guide provides a
## Choose a Ticket
1. Review the list of [Good First Contribution](https://mattermost.atlassian.net/issues/?filter=10206) tickets listed in Jira.
-
2. These projects are intended to be a straight forward first pull requests from new contributors.
If you don't find something appropriate for your interests, please see the full list of tickets [Accepting Pull Requests](https://mattermost.atlassian.net/issues/?filter=10101).
@@ -13,7 +12,7 @@ If you don't find something appropriate for your interests, please see the full
## Install Mattermost and set up a Fork
-1. Follow [developer setup instructions](https://github.com/mattermost/platform/blob/master/doc/install/dev-setup.md) to install Mattermost.
+1. Follow [developer setup instructions](https://github.com/mattermost/platform/blob/master/doc/developer/Setup.md) to install Mattermost.
2. Create a branch with <branch name> set to the ID of the ticket you're working on, for example ```PLT-394```, using command:
@@ -29,15 +28,18 @@ git checkout -b <branch name>
2. Please make sure to thoroughly test your change before submitting a pull request.
+ Please review the ["Fast, Obvious, Forgiving" experience design principles](http://www.mattermost.org/design-principles/) for Mattermost and check that your feature meets the criteria. Also, for any changes to user interface or help text, please read the changes out loud, as a quick and easy way to catch any inconsitencies.
+
+
## Submitting a Pull Request
1. Please add yourself to the Mattermost [approved contributor list](https://docs.google.com/spreadsheets/d/1NTCeG-iL_VS9bFqtmHSfwETo5f-8MQ7oMDE5IUYJi_Y/pubhtml?gid=0&single=true) prior to submitting by completing the [contributor license agreement](http://www.mattermost.org/mattermost-contributor-agreement/).
-2. When you submit your pull request please include the Ticket ID at the beginning of your pull request comment, followed by a colon.
+2. When you submit your pull request please make it against `master` and include the Ticket ID at the beginning of your pull request comment, followed by a colon.
For example, for a ticket ID `PLT-394` start your comment with: `PLT-394:`. See [previously closed pull requests](https://github.com/mattermost/platform/pulls?q=is%3Apr+is%3Aclosed) for examples.
-3. Once submitted, your pull request will be checked via an automated build process and will be reviewed by the Mattermost core team, who may either accept the PR or follow-up with feedback.
+3. Once submitted, your pull request will be checked via an automated build process and will be reviewed by at least two members of the Mattermost core team, who may either accept the PR or follow-up with feedback. It would then get merged into `master` for the next release.
4. If you've included your mailing address in Step 1, you'll be receiving a [Limited Edition Mattermost Mug](http://forum.mattermost.org/t/limited-edition-mattermost-mugs/143) as a thank you gift after your first pull request has been accepted.
diff --git a/doc/developer/Setup.md b/doc/developer/Setup.md
index c63bde512..afaef4ee4 100644
--- a/doc/developer/Setup.md
+++ b/doc/developer/Setup.md
@@ -11,7 +11,7 @@ Developer Machine Setup
`docker-machine ip dev`
3. Add a line to your /etc/hosts that goes `<Docker IP> dockerhost`
4. Run `docker-machine env dev` and copy the export statements to your ~/.bash_profile
-2. Download Go (version 1.4.2) from http://golang.org/dl/
+2. Download Go (version 1.4.2 or 1.5.1. Final release bits are built with 1.4.2) from http://golang.org/dl/
3. Set up your Go workspace
1. `mkdir ~/go`
2. Add the following to your ~/.bash_profile
@@ -51,7 +51,7 @@ Any issues? Please let us know on our forums at: http://forum.mattermost.org
`127.0.0.1 dockerhost`
3. Install build essentials
1. `apt-get install build-essential`
-4. Download Go (version 1.4.2) from http://golang.org/dl/
+4. Download Go (version 1.4.2 or 1.5.1. Final release bits are built with 1.4.2) from http://golang.org/dl/
5. Set up your Go workspace and add Go to the PATH
1. `mkdir ~/go`
2. Add the following to your ~/.bashrc
diff --git a/doc/help/Manage-Team.md b/doc/help/Manage-Members.md
index b45ea4283..04243f9fb 100644
--- a/doc/help/Manage-Team.md
+++ b/doc/help/Manage-Members.md
@@ -1,10 +1,10 @@
-# Manage Team
+# Manage Members
-The Manage Team menu is used to change the user roles assigned to members belonging to a team.
+The Manage Members menu is used to change the user roles assigned to members belonging to a team.
## User Roles
-The following user roles are assigned from the **Manage Team** menu option in the team site main menu.
+The following user roles are assigned from the **Manage Members** menu option in the team site main menu.
### System Admin
@@ -23,7 +23,7 @@ The Team Administrator is typically a non-technical end user and has the followi
- Access to the "Team Settings" menu from the team site main menu
- Ability to change the team name and import data from Slack export files
-- Access to the "Manage Team" menu and change user roles to the levels of Team Administrator, Member and Inactive
+- Access to the "Manage Members" menu and change user roles to the levels of Team Administrator, Member and Inactive
### Member
diff --git a/doc/help/README.md b/doc/help/README.md
index d07424c70..9271d64dd 100644
--- a/doc/help/README.md
+++ b/doc/help/README.md
@@ -8,5 +8,5 @@ _Note: Help documentation is a work-in-progress. Community contributions highly
You can access the **Team Site Main Menu** by clicking on the three vertical dots at the top of the left sidebar in a team site. Here we describe the various options available from the menu:
-- [Manage Teams](Manage-Team.md)
+- [Manage Members](Manage-Members.md)
diff --git a/doc/help/Slack-Import.md b/doc/help/Slack-Import.md
index d8d6170c4..a2914570b 100644
--- a/doc/help/Slack-Import.md
+++ b/doc/help/Slack-Import.md
@@ -1,18 +1,22 @@
-#### Slack Import (Alpha)
+### Slack Import (Beta)
*Note: As a SaaS service, Slack is able to change its export format quickly. If you encounter issues not mentioned in the documentation below, please let us know by [filing an issue](https://github.com/mattermost/platform/issues).*
-The Slack Import feature in Mattermost is in "Preview" and focus is on supporting migration of teams of less than 100 registered users. The feature can be accessed from by Team Administrators and Team Owners via the `Team Settings -> Import` menu option.
+#### Usage
+
+The Slack Import feature in Mattermost is in "Beta" and focus is on supporting migration of teams of less than 100 registered users. The feature can be accessed from by Team Administrators and Team Owners via the `Team Settings -> Import` menu option.
+
+- **It is highly recommended that you test Slack import before applying it to an instance intended for production.** If you use Docker, you can spin up a test instance in one line (`docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform`). If you don't use Docker, there are [step-by-step instructions to install Mattermost in preview mode in less than 5 minutes](../install/Docker-Single-Container.md).
Mattermost currently supports the processing of an "Export" file from Slack containing account information and public channel archives from a Slack team.
-- In the feature preview, emails and usernames from Slack are used to create new Mattermost accounts, connected to messages history in imported Slack channels. Users can activate these accounts and by going to the Password Reset screen in Mattermost to set new credentials.
-- Once logged in, users will have access to previous Slack messages shared in public channels, now imported to Mattermost.
+- Emails and usernames from Slack are used to create new Mattermost accounts, connected to messages history in imported Slack channels. Users can activate these accounts and by going to the Password Reset screen in Mattermost to set new credentials.
+- Once logged in, users will have access to previous Slack messages shared in public channels, now imported to Mattermost.
-Limitations:
+#### Limitations:
- Newly added markdown suppport in Slack's Posts 2.0 feature announced on September 28, 2015 is not yet supported.
- Slack does not export files or images your team has stored in Slack's database. Mattermost will provide links to the location of your assets in Slack's web UI.
- Slack does not export any content from private groups or direct messages that your team has stored in Slack's database.
-- The Preview release of Slack Import does not offer pre-checks or roll-back and will not import Slack accounts with username or email address collisions with existing Mattermost accounts. Also, Slack channel names with underscores will not import. Also, mentions do not yet resolve as Mattermost usernames (still show Slack ID).
+- The Beta release of Slack Import does not offer pre-checks or roll-back and will not import Slack accounts with username or email address collisions with existing Mattermost accounts. Also, Slack channel names with underscores will not import. Also, mentions do not yet resolve as Mattermost usernames (still shows Slack ID).
diff --git a/doc/install/Configuration-Settings.md b/doc/install/Configuration-Settings.md
new file mode 100644
index 000000000..a92893753
--- /dev/null
+++ b/doc/install/Configuration-Settings.md
@@ -0,0 +1,302 @@
+## System Console Settings
+
+The System Console user interface lets system administrators manage a Mattermost server and multiple teams from a web-based user interface. The first user added to a new Mattermost install is assigned the system administrator role and can access the System Console from the main menu of any team. Setting changes in the System Console are stored in `config.json`.
+
+### Service Settings
+
+General settings to configure the listening address, login security, testing, webhooks and service integration of Mattermost.
+
+#### System
+
+```"ListenAddress": ":8065"```
+The IP address on which to listen and the port on which to bind. Entering ":8065" will bind to all interfaces or you can choose one like "127.0.0.1:8065". Changing this will require a server restart before taking effect.
+
+```"MaximumLoginAttempts": 10```
+Failed login attempts allowed before a user is locked out and required to reset their password via email.
+
+```"SegmentDeveloperKey": ""```
+For users running SaaS services, signup for a key at Segment.com to track metrics.
+
+```"GoogleDeveloperKey": ""```
+Set this key to enable embedding of YouTube video previews based on hyperlinks appearing in messages or comments. Instructions to obtain a key available at https://www.youtube.com/watch?v=Im69kzhpR3I. Leaving the field blank disables the automatic generation of YouTube video previews from links.
+
+```"EnableTesting": false```
+"true": `/loadtest` slash command is enabled to load test accounts and test data.
+
+```"EnableSecurityFixAlert": true```
+”true”: System Administrators are notified by email if a relevant security fix alert has been announced in the last 12 hours. Requires email to be enabled.
+
+#### Webhooks
+
+```"EnableIncomingWebhooks": true```
+Developers building integrations can create webhook URLs for channels and private groups. Please see http://mattermost.org/webhooks to learn about creating webhooks, view samples, and to let the community know about integrations you have built. "true": Incoming webhooks will be allowed. To manage incoming webhooks, go to Account Settings -> Integrations. The webhook URLs created in Account Settings can be used by external applications to create posts in any channels or private groups that you have access to; “false”: The Integrations tab of Account Settings is hidden and incoming webhooks are disabled.
+
+Security note: By enabling this feature, users may be able to perform [phishing attacks](https://en.wikipedia.org/wiki/Phishing) by attempting to impersonate other users. To combat these attacks, a BOT tag appears next to all posts from a webhook. Enable at your own risk.
+
+```"EnablePostUsernameOverride": false```
+"true": Webhooks will be allowed to change the username they are posting as; “false”: Webhooks can only post as the username they were set up with. See http://mattermost.org/webhooks for more details.
+
+```"EnablePostIconOverride": false```
+"true": Webhooks will be allowed to change the icon they post with; “false”: Webhooks can only post with the profile picture of the account they were set up with. See http://mattermost.org/webhooks for more details.
+
+### Team Settings
+
+Settings to configure the appearance, size, and access options for teams.
+
+```"SiteName": "Mattermost"```
+Name of service shown in login screens and UI.
+
+```"MaxUsersPerTeam": 50```
+Maximum number of users per team, including both active and inactive users.
+
+```"EnableTeamCreation": true```
+"true": Ability to create a new team is enabled for all users; “false”: the ability to create teams is disabled. The Create A New Team button is hidden in the main menu UI.
+
+```"EnableUserCreation": true```
+"true": Ability to create new accounts is enabled via inviting new members or sharing the team invite link; “false”: the ability to create accounts is disabled. The create account button displays an error when trying to signup via an email invite or team invite link.
+
+```"RestrictCreationToDomains": ""```
+Teams can only be created by a verified email from this list of comma-separated domains (e.g. "corp.mattermost.com, mattermost.org").
+
+
+### SQL Settings
+
+Settings to configure the data sources, connections, and encryption of SQL databases. Changing properties in this section will require a server restart before taking effect.
+
+```"DriverName": "mysql"```
+"mysql": enables driver to MySQL database; "postgres": enables driver to PostgreSQL database. This setting can only be changed from config.json file, it cannot be changed from the System Console user interface.
+
+```"DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8"```
+This is the connection string to the master database. When **DriverName**="postgres" then use a connection string in the form “postgres://mmuser:password@localhost:5432/mattermost_test?sslmode=disable&connect_timeout=10”. This setting can only be changed from config.json file, it cannot be changed from the System Console user interface.
+
+```"DataSourceReplicas": []```
+This is a list of connection strings pointing to read replicas of MySQL or PostgreSQL database. If running a single server, set to DataSource. This setting can only be changed from config.json file, it cannot be changed from the System Console user interface.
+
+```"MaxIdleConns": 10```
+Maximum number of idle connections held open to the database.
+
+```"MaxOpenConns": 10```
+Maximum number of open connections held open to the database.
+
+```"Trace": false```
+"true": Executing SQL statements are written to the log for development.
+
+```"AtRestEncryptKey": "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QVg"```
+32-character (to be randomly generated via Admin Console) salt available to encrypt and decrypt sensitive fields in database.
+
+
+### Email Settings
+
+Settings to configure email signup, notifications, security, and SMTP options.
+
+#### Signup
+
+```"EnableSignUpWithEmail": true```
+"true": Allow team creation and account signup using email and password; “false”: Email signup is disabled and users are not able to invite new members. This limits signup to single-sign-on services like OAuth or LDAP.
+
+#### Notifications
+
+```"SendEmailNotifications": false```
+"true": Enables sending of email notifications. “false”: Disables email notifications for developers who may want to skip email setup for faster development.
+
+```"RequireEmailVerification": false```
+"true": Require email verification after account creation prior to allowing login; “false”: Users do not need to verify their email address prior to login. Developers may set this field to false so skip sending verification emails for faster development.
+
+```"FeedbackName": ""```
+Name displayed on email account used when sending notification emails from Mattermost system.
+
+```"FeedbackEmail": ""```
+Address displayed on email account used when sending notification emails from Mattermost system.
+
+#### SMTP
+
+```"SMTPUsername": ""```
+Obtain this credential from the administrator setting up your email server.
+
+```"SMTPPassword": ""```
+Obtain this credential from the administrator setting up your email server.
+
+```"SMTPServer": ""```
+Location of SMTP email server.
+
+```"SMTPPort": ""```
+Port of SMTP email server.
+
+#### Security
+
+```"ConnectionSecurity": ""```
+"none": Send email over an unsecure connection; "TLS": Communication between Mattermost and your email server is encrypted; “STARTTLS”: Attempts to upgrade an existing insecure connection to a secure connection using TLS.
+
+```"InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9YoS"```
+32-character (to be randomly generated via Admin Console) salt added to signing of email invites.
+
+
+```"PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL"```
+32-character (to be randomly generated via Admin Console) salt added to signing of password reset emails.
+
+
+### File Settings
+
+Settings to configure storage, appearance, and security of files and images.
+
+#### File Storage
+
+```"DriverName": "local"```
+System used for file storage. “local”: Files and images are stored on the local file system. “amazons3”: Files and images are stored on Amazon S3 based on the provided access key, bucket and region fields.
+
+```"Directory": "./data/"```
+Directory to which files are written. If blank, directory will be set to ./data/.
+
+```"AmazonS3AccessKeyId": ""```
+Obtain this credential from your Amazon EC2 administrator.
+
+```"AmazonS3SecretAccessKey": ""```
+Obtain this credential from your Amazon EC2 administrator.
+
+```"AmazonS3Bucket": ""```
+Name you selected for your S3 bucket in AWS.
+
+```"AmazonS3Region": ""```
+AWS region you selected for creating your S3 bucket. Refer to [AWS Reference Documentation](http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region) and choose this variable from the Region column.
+
+#### Image Settings
+
+```"ThumbnailWidth": 120```
+Width of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.
+
+```"ThumbnailHeight": 100```
+Height of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.
+
+```"PreviewWidth": 1024```
+Maximum width of preview image. Updating this value changes how preview images render in future, but does not change images created in the past.
+
+```"PreviewHeight": 0```
+Maximum height of preview image ("0": Sets to auto-size). Updating this value changes how preview images render in future, but does not change images created in the past.
+
+```"ProfileWidth": 128```
+The width to which profile pictures are resized after being uploaded via Account Settings.
+
+```"ProfileHeight": 128```
+The height to which profile pictures are resized after being uploaded via Account Settings.
+
+```"EnablePublicLink": true```
+”true”: Allow users to share public links to files and images when previewing; “false”: The Get Public Link option is hidden from the image preview user interface.
+
+```"PublicLinkSalt": "A705AklYF8MFDOfcwh3I488G8vtLlVip"```
+32-character (to be randomly generated via Admin Console) salt added to signing of public image links.
+
+
+### Log Settings
+
+Settings to configure the console and log file output, detail level, format and location of error messages.
+
+#### Console Settings
+
+```"EnableConsole": true```
+“true”: Output log messages to the console based on **ConsoleLevel** option. The server writes messages to the standard output stream (stdout).
+
+```"ConsoleLevel": "DEBUG"```
+Level of detail at which log events are written to the console when **EnableConsole**=true. ”ERROR”: Outputs only error messages; “INFO”: Outputs error messages and information around startup and initialization; “DEBUG”: Prints high detail for developers debugging issues.
+
+#### Log File Settings
+
+```"EnableFile": true```
+”true”: Log files are written to files specified in **FileLocation**.
+
+```"FileLevel": "INFO"```
+Level of detail at which log events are written to log files when **EnableFile**=true. “ERROR”: Outputs only error messages; “INFO”: Outputs error messages and information around startup and initialization; “DEBUG”: Prints high detail for developers debugging issues.
+
+```"FileFormat": ""```
+Format of log message output. If blank, **FileFormat** = "[%D %T] [%L] (%S) %M", where:
+
+ %T Time (15:04:05 MST)
+ %t Time (15:04)
+ %D Date (2006/01/02)
+ %d Date (01/02/06)
+ %L Level (FNST, FINE, DEBG, TRAC, WARN, EROR, CRIT)
+ %S Source
+ %M Message
+
+```"FileLocation": ""```
+Directory to which log files are written. If blank, log files write to ./logs/mattermost/mattermost.log. Log rotation is enabled and every 10,000 lines of log information is written to new files stored in the same directory, for example mattermost.2015-09-23.001, mattermost.2015-09-23.002, and so forth.
+
+### Rate Limit Settings
+
+Settings to enable API rate limiting and configure requests per second, user sessions and variables for rate limiting. Changing properties in this section will require a server restart before taking effect.
+
+```"EnableRateLimiter": true```
+”true”: APIs are throttled at the rate specified by **PerSec**.
+
+```"PerSec": 10```
+Throttle API at this number of requests per second if **EnableRateLimiter**=true.
+
+```"MemoryStoreSize": 10000```
+Maximum number of user sessions connected to the system as determined by **VaryByRemoteAddr** and **VaryByHeader** variables.
+
+```"VaryByRemoteAddr": true```
+"true": Rate limit API access by IP address.
+
+```"VaryByHeader": ""```
+Vary rate limiting by HTTP header field specified (e.g. when configuring Ngnix set to "X-Real-IP", when configuring AmazonELB set to "X-Forwarded-For").
+
+### Privacy Settings
+
+Settings to configure the name and email privacy of users on your system.
+
+```"ShowEmailAddress": true```
+“true”: Show email address of all users; "false": Hide email address of users from other users in the user interface, including team owners and team administrators. This is designed for managing teams where users choose to keep their contact information private.
+
+```"ShowFullName": true```
+”true”: Show full name of all users; “false”: hide full name of users from other users including team owner and team administrators.
+
+### GitLab Settings
+
+Settings to configure account and team creation using GitLab OAuth.
+
+```"Enable": false```
+“true”: Allow team creation and account signup using GitLab OAuth. To configure, input the **Secret** and **Id** credentials.
+
+```"Secret": ""```
+Obtain this value by logging into your GitLab account. Go to Profile Settings -> Applications -> New Application, enter a Name, then enter Redirect URLs "https://<your-mattermost-url>/login/gitlab/complete" (example: https://example.com:8065/login/gitlab/complete) and "https://<your-mattermost-url>/signup/gitlab/complete".
+
+```"Id": ""```
+Obtain this value by logging into your GitLab account. Go to Profile Settings -> Applications -> New Application, enter a Name, then enter Redirect URLs "https://<your-mattermost-url>/login/gitlab/complete" (example: https://example.com:8065/login/gitlab/complete) and "https://<your-mattermost-url>/signup/gitlab/complete".
+
+```"AuthEndpoint": ""```
+Enter https://<your-gitlab-url>/oauth/authorize (example: https://example.com:3000/oauth/authorize). Use HTTP or HTTPS depending on how your server is configured.
+
+```"TokenEndpoint": ""```
+Enter https://<your-gitlab-url>/oauth/authorize (example: https://example.com:3000/oauth/token). Use HTTP or HTTPS depending on how your server is configured.
+
+```"UserApiEndpoint": ""```
+Enter https://<your-gitlab-url>/oauth/authorize (example: https://example.com:3000/api/v3/user). Use HTTP or HTTPS depending on how your server is configured.
+
+## Config.json Settings Not in System Console
+
+System Console allows an IT Admin to update settings defined in `config.json`. However there are a number of settings in `config.json` unavailable in the System Console and require update from the file itself. We describe them here:
+
+### Service Settings
+
+```"EnableOAuthServiceProvider": false```
+”true”: Allow Mattermost to function as an OAuth provider, allowing 3rd party apps access to your user store for authentication.
+
+### Push Notification Settings
+
+```"ApplePushServer": ""```
+Setting for features in development.
+
+```"ApplePushCertPublic": ""```
+Setting for features in development.
+
+```"ApplePushCertPrivate": ""```
+Setting for features in development.
+
+### File Settings
+
+```"InitialFont": "luximbi.ttf"```
+Font used in auto-generated profile pics with colored backgrounds.
+
+### GitLab Settings
+
+```"Scope": ""```
+Standard setting for OAuth to determine the scope of information shared with OAuth client. Not currently supported by GitLab OAuth.
diff --git a/doc/install/Release-Numbering.md b/doc/install/Release-Numbering.md
index 71374f7ef..3b0391cff 100644
--- a/doc/install/Release-Numbering.md
+++ b/doc/install/Release-Numbering.md
@@ -14,4 +14,4 @@ Major Build Number:
Minor Build Number:
-- Indicates bugfix/security releases (e.g. 1.2.5, 1.2.6)
+- Indicates a bug fix or security release (e.g. 1.2.5, 1.2.6)
diff --git a/doc/install/SMTP-Email-Setup.md b/doc/install/SMTP-Email-Setup.md
index 86e2bb20e..fcf012804 100644
--- a/doc/install/SMTP-Email-Setup.md
+++ b/doc/install/SMTP-Email-Setup.md
@@ -39,3 +39,17 @@ To enable email, turn this option off by setting `ByPassEmail=false` and configu
2. Kill the process `kill pid`
3. The service should restart automatically. Verify the Mattermost service is running with `ps -A`
4. Current logged in users will not be affected, but upon logging out or session expiration users will be required to verify their email address.
+
+### Troubleshooting SMTP
+
+If you have issues with your SMTP install, from your Mattermost team site go to the main menu and open `System Console > Logs` to look for error messages related to your setup. You can do a search for the error code to narrow down the issue. Sometimes ISPs require nuanced setups for SMTP and error codes can hint at how to make the proper adjustments.
+
+For example, if `System Console > Logs` has an error code reading:
+
+```
+Connection unsuccessful: Failed to add to email address - 554 5.7.1 <unknown[IP-ADDRESS]>: Client host rejected: Access denied
+```
+
+Search for `554 5.7.1 error` and `Client host rejected: Access denied`.
+
+
diff --git a/doc/install/Troubleshooting.md b/doc/install/Troubleshooting.md
new file mode 100644
index 000000000..8d82100d8
--- /dev/null
+++ b/doc/install/Troubleshooting.md
@@ -0,0 +1,7 @@
+### Mattermost Troubleshooting
+
+#### 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.
+
diff --git a/doc/integrations/Single-Sign-On/Gitlab.md b/doc/integrations/Single-Sign-On/Gitlab.md
index 87889df66..e503a158b 100644
--- a/doc/integrations/Single-Sign-On/Gitlab.md
+++ b/doc/integrations/Single-Sign-On/Gitlab.md
@@ -12,10 +12,12 @@ The following steps can be used to configure Mattermost to use GitLab as a singl
3. Submit the application and copy the given _Id_ and _Secret_ into the appropriate _SSOSettings_ fields in config/config.json
4. Also in config/config.json, set _Allow_ to `true` for the _gitlab_ section, leave _Scope_ blank and use the following for the endpoints:
- * _AuthEndpoint_: `<your-gitlab-url>/oauth/authorize` (example http://localhost:3000/oauth/authorize)
- * _TokenEndpoint_: `<your-gitlab-url>/oauth/token`
- * _UserApiEndpoint_: `<your-gitlab-url>/api/v3/user`
+ * _AuthEndpoint_: `https://<your-gitlab-url>/oauth/authorize` (example https://example.com/oauth/authorize)
+ * _TokenEndpoint_: `https://<your-gitlab-url>/oauth/token`
+ * _UserApiEndpoint_: `https://<your-gitlab-url>/api/v3/user`
+
+ Note: Make sure your `HTTPS` or `HTTP` prefix for endpoint URLs matches how your server configuration.
-6. (Optional) If you would like to force all users to sign-up with GitLab only, in the _ServiceSettings_ section of config/config.json please set _DisableEmailSignUp_ to `true`.
+5. (Optional) If you would like to force all users to sign-up with GitLab only, in the _ServiceSettings_ section of config/config.json please set _DisableEmailSignUp_ to `true`.
-7. Restart your Mattermost server to see the changes take effect.
+6. Restart your Mattermost server to see the changes take effect.
diff --git a/doc/integrations/webhooks/Incoming-Webhooks.md b/doc/integrations/webhooks/Incoming-Webhooks.md
index 645d4861e..c6323a24a 100644
--- a/doc/integrations/webhooks/Incoming-Webhooks.md
+++ b/doc/integrations/webhooks/Incoming-Webhooks.md
@@ -1,19 +1,19 @@
# Incoming Webhooks
-Incoming webhooks allow external applications, written in the programming language of your choice--to post messages into Mattermost channels and private groups by sending a specifically formatted JSON payload via HTTP POST request to a secret Mattermost URL generated specifically for each application.
+Incoming webhooks allow external applications, written in the programming language of your choice--to post messages into Mattermost channels and private groups by sending a specifically formatted JSON payload via HTTP POST request to a secret Mattermost URL generated specifically for each application.
-A couple key points:
+A couple key points:
-- **Mattermost incoming webhooks are Slack-compatible.** If you've used Slack's incoming webhooks to create integrations, you can copy and paste that code to create Mattermost integrations. Mattermost automatically translates Slack's propretiary JSON payload format into markdown to render in Mattermost messages.
+- **Mattermost incoming webhooks are Slack-compatible.** If you've used Slack's incoming webhooks to create integrations, you can copy and paste that code to create Mattermost integrations. Mattermost automatically translates Slack's propretiary JSON payload format into markdown to render in Mattermost messages.
- **Mattermost incoming webhooks support full markdown.** A rich range of formatting unavailable in Slack is made possible through [markdown support](../../usage/Markdown.md) in Mattermost, incuding headings, formatted fonts, tables, inline images and other options supported by [Mattermost Markdown].
_Example:_
-Suppose you wanted to create a notification of the status of a daily build, with a table of total tests run and total tests failed by component category, with links to failed tests by category. You could create the following JSON payload to post to a Mattermost channel using webhooks:
+Suppose you wanted to create a notification of the status of a daily build, with a table of total tests run and total tests failed by component category, with links to failed tests by category. You could create the following JSON payload to post to a Mattermost channel using webhooks:
```
payload={"text": "
-***
+***
##### Build Break - Project X - December 12, 2015 - 15:32 GMT +0
| Component | Tests Run | Tests Failed |
|:-----------|:------------|:-----------------------------------------------|
@@ -25,56 +25,68 @@ payload={"text": "
```
Which would render in a Mattermost message as follows:
-***
+***
##### Build Break - Project X - December 12, 2015 - 15:32 GMT +0
| Component | Tests Run | Tests Failed |
|:------------ |:---------------|:-----|
| Server | 948 | :white_check_mark: 0 |
| Web Client | 123 | :warning: [2 (see details)](http://linktologs) |
| iOS Client | 78 | :warning: [3 (see details)](http://linktologs) |
-***
+***
+
+### Enabling Incoming Webhooks
+Incoming webhooks should be enabled on your Mattermost instance by default, but if they are not you'll need to get your system administrator to enable them. If you are the system administrator you can enable them by doing the following:
+
+1. Login to your Mattermost team account that has the system administrator role.
+1. Enable incoming webhooks from **System Console -> Service Settings**.
+1. (Optional) Configure the **Enable Overriding of Usernames from Webhooks** option to allow external applications to post messages under any name. If not enabled, the username of the creator of the webhook URL is used to post messages.
+2. (Optional) Configure the **Enable Overriding of Icon from Webhooks** option to allow external applciations to change the icon of the account posting messages. If not enabled, the icon of the creator of the webhook URL is used to post messages.
+
+### Setting Up Existing Integrations
+If you've already found or built an integration and are just looking to hook it up, then you should just need to follow the specific instructions of that integration. If the integration is using Mattermost incoming webhooks, then at some point in the instructions it will ask for a webhook URL. You can get this URL by following the first step in the next section _Creating Integrations using Incoming Webhooks_.
### Creating Integrations using Incoming Webhooks
-You can create a webhook integration to post into Mattermost channels and private groups using these steps:
+You can create a webhook integration to post into Mattermost channels and private groups using these steps:
-1. Enable incoming webhooks from **System Console -> Service Settings**
+**Note: Incoming webhooks must be enabled. Only your Mattermost system administrator can enable incoming webhooks if they are currently disabled.**
- 1. Optionally configure the **Enable Overriding of Usernames from Webhooks** option to allow external applications to post messages under any name. If not enabled, the username of the creator of the webhook URL is used to post messages.
- 2. Optionally configure the **Enable Overriding of Icon from Webhooks** option to allow external applciations to change the icon of the account posting messages. If not enabled, the icon of the creator of the webhook URL is used to post messages.
-
2. Create a Mattermost Incoming Webhook URL
1. Login to your Mattermost team site and go to **Account Settings -> Integrations**
2. Next to **Incoming Webhooks** click **Edit**
3. Select the channel or private group to receive webhook payloads, then click **Add** to create the webhook
-
-3. Write a function to call the URL with a properly formatted JSON payload
- 1. To make sure everything works, try a curl command from your terminal or command line to send a JSON string as the `payload` parameter in a HTTP POST request.
- - _Example:_
+ 4. To see your new webhook in action, try a curl command from your terminal or command-line to send a JSON string as the `payload` parameter in a HTTP POST request.
+ 1. Example:
```
curl -i -X POST -d 'payload={"text": "Hello, this is some text."}' http://yourmattermost.com/hooks/xxx-generatedkey-xxx
-```
- 2. In addition, with **Enable Overriding of Usernames from Webhooks** turned on, you can also override the username the message posts as by providing a `username` parameter in your JSON payload. For example, you might want your message looking like it came from a robot so you can use ```payload={"username": "robot", "text": "Hello, this is some text."}``` to change the username of the post to robot. Note, to combat any malicious users from trying to use this to perform [phishing attacks](https://en.wikipedia.org/wiki/Phishing) a `BOT` indicator appears next to posts coming from incoming webhooks.
- 3. With **Enable Overriding of Icon from Webhooks** turned on, you can similarly change the icon the message posts with by providing a link to an image in the `icon_url` parameter of your payload. For example, ```payload={"icon_url": "http://somewebsite.com/somecoolimage.jpg", "text": "Hello, this is some text."}``` will post using whatever image is located at `http://somewebsite.com/somecoolimage.jpg` as the icon for the post.
+```
- 2. Set up your integration running on your own machine or a hosting service like AWS or Heroku, to start sending real time updates to Mattermost channels and private groups.
+3. Build your integration in the programming language of your choice
+ 1. Most integrations will be used to translate some sort of output from another system to an appropriately formatted input that will be passed into the Mattermost webhook URL. For example, an integration could take events generated by [GitLab outgoing webhooks](http://doc.gitlab.com/ee/web_hooks/web_hooks.html) and parse them into a JSON body to post into Mattermost.
+ 1. To get the message posted into Mattermost, your integration will need to create an HTTP POST request that will submit to the incoming webhook URL you created before. The body of the request must have a `payload` that contains a JSON object that specifies a `text` parameter. For example, `payload={"text": "Hello, this is some text."}` is a valid body for a request.
+ 2. Setup your integration running on Heroku, an AWS server or a server of your own to start sending real time updates to Mattermost channels and private groups.
Additional Notes:
-1. For the JSON payload, if `Content-Type` is specified as `application/json` in the headers of the HTTP request then the body of the request can be direct JSON. ```{"text": "Hello, this is some text."}```
+1. For the HTTP request body, if `Content-Type` is specified as `application/json` in the headers of the HTTP request then the body of the request can be direct JSON. For example, ```{"text": "Hello, this is some text."}```
+
+2. You can override the channel specified in the webhook definition by specifying a `channel` parameter in your payload. For example, you might have a single webhook created for _Town Square_, but you can use ```payload={"channel": "off-topic", "text": "Hello, this is some text."}``` to send a message to the _Off-Topic_ channel using the same webhook URL.
-2. You can override the channel specified in the webhook definition by specifying a `channel` parameter in your payload. For example, you might have a single webhook created for _Town Square_, but you can use ```payload={"channel": "off-topic", "text": "Hello, this is some text."}``` to send a message to the _Off-Topic_ channel using the same webhook URL.
+1. In addition, with **Enable Overriding of Usernames from Webhooks** turned on, you can also override the username the message posts as by providing a `username` parameter in your JSON payload. For example, you might want your message looking like it came from a robot so you can use ```payload={"username": "robot", "text": "Hello, this is some text."}``` to change the username of the post to robot. Note, to combat any malicious users from trying to use this to perform [phishing attacks](https://en.wikipedia.org/wiki/Phishing) a `BOT` indicator appears next to posts coming from incoming webhooks.
-3. Also, as mentioned previously, [markdown](../../usage/Markdown.md) can be used to create richly formatted payloads, for example: ```payload={"text": "# A Header\nThe _text_ below **the** header."}``` creates a messages with a header, a carriage return and bold text for "the".
+2. With **Enable Overriding of Icon from Webhooks** turned on, you can similarly change the icon the message posts with by providing a link to an image in the `icon_url` parameter of your payload. For example, ```payload={"icon_url": "http://somewebsite.com/somecoolimage.jpg", "text": "Hello, this is some text."}``` will post using whatever image is located at `http://somewebsite.com/somecoolimage.jpg` as the icon for the post.
+
+3. Also, as mentioned previously, [markdown](../../usage/Markdown.md) can be used to create richly formatted payloads, for example: ```payload={"text": "# A Header\nThe _text_ below **the** header."}``` creates a messages with a header, a carriage return and bold text for "the".
4. Just like regular posts, the text will be limited to 4000 characters at maximum.
-### Slack Compatibility
+### Slack Compatibility
-As mentioned above, Mattermost makes it easy to take integrations written for Slack's proprietary JSON payload format and repurpose them to become Mattermost integrations. The following automatic translations are supported:
+As mentioned above, Mattermost makes it easy to take integrations written for Slack's proprietary JSON payload format and repurpose them to become Mattermost integrations. The following automatic translations are supported:
-1. Payloads designed for Slack using `<>` to note the need to hyperlink a URL, such as ```payload={"text": "<http://www.mattermost.com/>"}```, are translated to the equivalent markdown in Mattermost and rendered the same as you would see in Slack.
-2. Similiarly, payloads designed for Slack using `|` within a `<>` to define linked text, such as ```payload={"text": "Click <http://www.mattermost.com/|here> for a link."}```, are also translated to the equivalent markdown in Mattermost and rendered the same as you would see in Slack.
+1. Payloads designed for Slack using `<>` to note the need to hyperlink a URL, such as ```payload={"text": "<http://www.mattermost.com/>"}```, are translated to the equivalent markdown in Mattermost and rendered the same as you would see in Slack.
+2. Similiarly, payloads designed for Slack using `|` within a `<>` to define linked text, such as ```payload={"text": "Click <http://www.mattermost.com/|here> for a link."}```, are also translated to the equivalent markdown in Mattermost and rendered the same as you would see in Slack.
3. Like Slack, by overriding the channel name with an @username, such as payload={"text": "Hi", channel: "@jim"}, you can send the message to a user through your direct message chat.
4. Channel names can be prepended with a #, like they are in Slack incoming webhooks, and the message will still be sent to the correct channel.
-To learn more about Incoming Webhooks and to see samples and community contributions, please visit <http://mattermost.org/webhooks>
+To see samples and community contributions, please visit <http://mattermost.org/webhooks>.
+
diff --git a/docker/0.7/Dockerrun.aws.zip b/docker/0.7/Dockerrun.aws.zip
deleted file mode 100644
index bba04cca5..000000000
--- a/docker/0.7/Dockerrun.aws.zip
+++ /dev/null
Binary files differ
diff --git a/docker/0.7/config_docker.json b/docker/0.7/config_docker.json
deleted file mode 100644
index cbac2ea69..000000000
--- a/docker/0.7/config_docker.json
+++ /dev/null
@@ -1,103 +0,0 @@
-{
- "LogSettings": {
- "ConsoleEnable": true,
- "ConsoleLevel": "INFO",
- "FileEnable": true,
- "FileLevel": "INFO",
- "FileFormat": "",
- "FileLocation": ""
- },
- "ServiceSettings": {
- "SiteName": "Mattermost",
- "Mode" : "dev",
- "AllowTesting" : true,
- "UseSSL": false,
- "Port": "80",
- "Version": "developer",
- "Shards": {
- },
- "InviteSalt": "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6",
- "PublicLinkSalt": "TO3pTyXIZzwHiwyZgGql7lM7DG3zeId4",
- "ResetSalt": "IPxFzSfnDFsNsRafZxz8NaYqFKhf9y2t",
- "AnalyticsUrl": "",
- "UseLocalStorage": true,
- "StorageDirectory": "/mattermost/data/",
- "AllowedLoginAttempts": 10,
- "DisableEmailSignUp": false
- },
- "SSOSettings": {
- "gitlab": {
- "Allow": false,
- "Secret" : "",
- "Id": "",
- "Scope": "",
- "AuthEndpoint": "",
- "TokenEndpoint": "",
- "UserApiEndpoint": ""
- }
- },
- "SqlSettings": {
- "DriverName": "mysql",
- "DataSource": "mmuser:mostest@tcp(localhost:3306)/mattermost_test?charset=utf8mb4,utf8",
- "DataSourceReplicas": ["mmuser:mostest@tcp(localhost:3306)/mattermost_test?charset=utf8mb4,utf8"],
- "MaxIdleConns": 10,
- "MaxOpenConns": 10,
- "Trace": false,
- "AtRestEncryptKey": "Ya0xMrybACJ3sZZVWQC7e31h5nSDWZFS"
- },
- "AWSSettings": {
- "S3AccessKeyId": "",
- "S3SecretAccessKey": "",
- "S3Bucket": "",
- "S3Region": ""
- },
- "ImageSettings": {
- "ThumbnailWidth": 120,
- "ThumbnailHeight": 100,
- "PreviewWidth": 1024,
- "PreviewHeight": 0,
- "ProfileWidth": 128,
- "ProfileHeight": 128,
- "InitialFont": "luximbi.ttf"
- },
- "EmailSettings": {
- "ByPassEmail" : true,
- "SMTPUsername": "",
- "SMTPPassword": "",
- "SMTPServer": "",
- "UseTLS": false,
- "UseStartTLS": false,
- "FeedbackEmail": "",
- "FeedbackName": "",
- "ApplePushServer": "",
- "ApplePushCertPublic": "",
- "ApplePushCertPrivate": ""
- },
- "RateLimitSettings": {
- "UseRateLimiter": true,
- "PerSec": 10,
- "MemoryStoreSize": 10000,
- "VaryByRemoteAddr": true,
- "VaryByHeader": ""
- },
- "PrivacySettings": {
- "ShowEmailAddress": true,
- "ShowPhoneNumber": true,
- "ShowSkypeId": true,
- "ShowFullName": true
- },
- "TeamSettings": {
- "MaxUsersPerTeam": 150,
- "AllowPublicLink": true,
- "AllowValetDefault": false,
- "TermsLink": "/static/help/configure_links.html",
- "PrivacyLink": "/static/help/configure_links.html",
- "AboutLink": "/static/help/configure_links.html",
- "HelpLink": "/static/help/configure_links.html",
- "ReportProblemLink": "/static/help/configure_links.html",
- "TourLink": "/static/help/configure_links.html",
- "DefaultThemeColor": "#2389D7",
- "DisableTeamCreation": false,
- "RestrictCreationToDomains": ""
- }
-}
diff --git a/docker/0.7/Dockerfile b/docker/1.1/Dockerfile
index 202d42dbc..855dd4375 100644
--- a/docker/0.7/Dockerfile
+++ b/docker/1.1/Dockerfile
@@ -34,7 +34,7 @@ VOLUME /var/lib/mysql
WORKDIR /mattermost
# Copy over files
-ADD https://github.com/mattermost/platform/releases/download/v0.7.1/mattermost.tar.gz /
+ADD https://github.com/mattermost/platform/releases/download/v1.1.0-rc2/mattermost.tar.gz /
RUN tar -zxvf /mattermost.tar.gz --strip-components=1 && rm /mattermost.tar.gz
ADD config_docker.json /
ADD docker-entry.sh /
diff --git a/docker/1.1/Dockerrun.aws.zip b/docker/1.1/Dockerrun.aws.zip
new file mode 100644
index 000000000..945168a71
--- /dev/null
+++ b/docker/1.1/Dockerrun.aws.zip
Binary files differ
diff --git a/docker/0.7/Dockerrun.aws/.ebextensions/01_files.config b/docker/1.1/Dockerrun.aws/.ebextensions/01_files.config
index 7f40a8b34..7f40a8b34 100644
--- a/docker/0.7/Dockerrun.aws/.ebextensions/01_files.config
+++ b/docker/1.1/Dockerrun.aws/.ebextensions/01_files.config
diff --git a/docker/0.7/Dockerrun.aws/Dockerrun.aws.json b/docker/1.1/Dockerrun.aws/Dockerrun.aws.json
index b53ec235c..042e79bd3 100755
--- a/docker/0.7/Dockerrun.aws/Dockerrun.aws.json
+++ b/docker/1.1/Dockerrun.aws/Dockerrun.aws.json
@@ -1,7 +1,7 @@
{
"AWSEBDockerrunVersion": "1",
"Image": {
- "Name": "mattermost/platform:0.7",
+ "Name": "mattermost/platform:1.1",
"Update": "true"
},
"Ports": [
diff --git a/docker/1.1/config_docker.json b/docker/1.1/config_docker.json
new file mode 100644
index 000000000..653b6ffd7
--- /dev/null
+++ b/docker/1.1/config_docker.json
@@ -0,0 +1,92 @@
+{
+ "ServiceSettings": {
+ "ListenAddress": ":80",
+ "MaximumLoginAttempts": 10,
+ "SegmentDeveloperKey": "",
+ "GoogleDeveloperKey": "",
+ "EnableOAuthServiceProvider": false,
+ "EnableIncomingWebhooks": true,
+ "EnablePostUsernameOverride": false,
+ "EnablePostIconOverride": false,
+ "EnableTesting": false,
+ "EnableSecurityFixAlert": true
+ },
+ "TeamSettings": {
+ "SiteName": "Mattermost",
+ "MaxUsersPerTeam": 50,
+ "EnableTeamCreation": true,
+ "EnableUserCreation": true,
+ "RestrictCreationToDomains": ""
+ },
+ "SqlSettings": {
+ "DriverName": "mysql",
+ "DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8",
+ "DataSourceReplicas": [],
+ "MaxIdleConns": 10,
+ "MaxOpenConns": 10,
+ "Trace": false,
+ "AtRestEncryptKey": "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QVg"
+ },
+ "LogSettings": {
+ "EnableConsole": false,
+ "ConsoleLevel": "INFO",
+ "EnableFile": true,
+ "FileLevel": "INFO",
+ "FileFormat": "",
+ "FileLocation": ""
+ },
+ "FileSettings": {
+ "DriverName": "local",
+ "Directory": "/mattermost/data/",
+ "EnablePublicLink": true,
+ "PublicLinkSalt": "A705AklYF8MFDOfcwh3I488G8vtLlVip",
+ "ThumbnailWidth": 120,
+ "ThumbnailHeight": 100,
+ "PreviewWidth": 1024,
+ "PreviewHeight": 0,
+ "ProfileWidth": 128,
+ "ProfileHeight": 128,
+ "InitialFont": "luximbi.ttf",
+ "AmazonS3AccessKeyId": "",
+ "AmazonS3SecretAccessKey": "",
+ "AmazonS3Bucket": "",
+ "AmazonS3Region": ""
+ },
+ "EmailSettings": {
+ "EnableSignUpWithEmail": true,
+ "SendEmailNotifications": false,
+ "RequireEmailVerification": false,
+ "FeedbackName": "",
+ "FeedbackEmail": "",
+ "SMTPUsername": "",
+ "SMTPPassword": "",
+ "SMTPServer": "",
+ "SMTPPort": "",
+ "ConnectionSecurity": "",
+ "InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9YoS",
+ "PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL",
+ "ApplePushServer": "",
+ "ApplePushCertPublic": "",
+ "ApplePushCertPrivate": ""
+ },
+ "RateLimitSettings": {
+ "EnableRateLimiter": true,
+ "PerSec": 10,
+ "MemoryStoreSize": 10000,
+ "VaryByRemoteAddr": true,
+ "VaryByHeader": ""
+ },
+ "PrivacySettings": {
+ "ShowEmailAddress": true,
+ "ShowFullName": true
+ },
+ "GitLabSettings": {
+ "Enable": false,
+ "Secret": "",
+ "Id": "",
+ "Scope": "",
+ "AuthEndpoint": "",
+ "TokenEndpoint": "",
+ "UserApiEndpoint": ""
+ }
+}
diff --git a/docker/0.7/docker-entry.sh b/docker/1.1/docker-entry.sh
index 6bd2a1263..6bd2a1263 100755
--- a/docker/0.7/docker-entry.sh
+++ b/docker/1.1/docker-entry.sh
diff --git a/docker/dev/config_docker.json b/docker/dev/config_docker.json
index ab5b0a7be..653b6ffd7 100644
--- a/docker/dev/config_docker.json
+++ b/docker/dev/config_docker.json
@@ -8,7 +8,8 @@
"EnableIncomingWebhooks": true,
"EnablePostUsernameOverride": false,
"EnablePostIconOverride": false,
- "EnableTesting": false
+ "EnableTesting": false,
+ "EnableSecurityFixAlert": true
},
"TeamSettings": {
"SiteName": "Mattermost",
@@ -77,8 +78,7 @@
},
"PrivacySettings": {
"ShowEmailAddress": true,
- "ShowFullName": true,
- "EnableSecurityFixAlert": true
+ "ShowFullName": true
},
"GitLabSettings": {
"Enable": false,
diff --git a/docker/local/config_docker.json b/docker/local/config_docker.json
index ab5b0a7be..653b6ffd7 100644
--- a/docker/local/config_docker.json
+++ b/docker/local/config_docker.json
@@ -8,7 +8,8 @@
"EnableIncomingWebhooks": true,
"EnablePostUsernameOverride": false,
"EnablePostIconOverride": false,
- "EnableTesting": false
+ "EnableTesting": false,
+ "EnableSecurityFixAlert": true
},
"TeamSettings": {
"SiteName": "Mattermost",
@@ -77,8 +78,7 @@
},
"PrivacySettings": {
"ShowEmailAddress": true,
- "ShowFullName": true,
- "EnableSecurityFixAlert": true
+ "ShowFullName": true
},
"GitLabSettings": {
"Enable": false,
diff --git a/mattermost.go b/mattermost.go
index 6c0f0a1bf..d2a0567f4 100644
--- a/mattermost.go
+++ b/mattermost.go
@@ -81,28 +81,28 @@ func main() {
func securityAndDiagnosticsJob() {
go func() {
for {
- if utils.Cfg.PrivacySettings.EnableSecurityFixAlert && model.IsOfficalBuild() {
+ if *utils.Cfg.ServiceSettings.EnableSecurityFixAlert {
if result := <-api.Srv.Store.System().Get(); result.Err == nil {
props := result.Data.(model.StringMap)
lastSecurityTime, _ := strconv.ParseInt(props["LastSecurityTime"], 10, 0)
currentTime := model.GetMillis()
- id := props["DiagnosticId"]
- if len(id) == 0 {
- id = model.NewId()
- systemId := &model.System{Name: "DiagnosticId", Value: id}
- <-api.Srv.Store.System().Save(systemId)
- }
+ if (currentTime - lastSecurityTime) > 1000*60*60*24*1 {
+ l4g.Debug("Checking for security update from Mattermost")
- v := url.Values{}
- v.Set(utils.PROP_DIAGNOSTIC_ID, id)
- v.Set(utils.PROP_DIAGNOSTIC_BUILD, model.CurrentVersion+"."+model.BuildNumber)
- v.Set(utils.PROP_DIAGNOSTIC_DATABASE, utils.Cfg.SqlSettings.DriverName)
- v.Set(utils.PROP_DIAGNOSTIC_OS, runtime.GOOS)
- v.Set(utils.PROP_DIAGNOSTIC_CATEGORY, utils.VAL_DIAGNOSTIC_CATEGORY_DEFAULT)
+ id := props["DiagnosticId"]
+ if len(id) == 0 {
+ id = model.NewId()
+ systemId := &model.System{Name: "DiagnosticId", Value: id}
+ <-api.Srv.Store.System().Save(systemId)
+ }
- if (currentTime - lastSecurityTime) > 1000*60*60*24*1 {
- l4g.Info("Checking for security update from Mattermost")
+ v := url.Values{}
+ v.Set(utils.PROP_DIAGNOSTIC_ID, id)
+ v.Set(utils.PROP_DIAGNOSTIC_BUILD, model.CurrentVersion+"."+model.BuildNumber)
+ v.Set(utils.PROP_DIAGNOSTIC_DATABASE, utils.Cfg.SqlSettings.DriverName)
+ v.Set(utils.PROP_DIAGNOSTIC_OS, runtime.GOOS)
+ v.Set(utils.PROP_DIAGNOSTIC_CATEGORY, utils.VAL_DIAGNOSTIC_CATEGORY_DEFAULT)
systemSecurityLastTime := &model.System{Name: "LastSecurityTime", Value: strconv.FormatInt(currentTime, 10)}
if lastSecurityTime == 0 {
@@ -111,6 +111,14 @@ func securityAndDiagnosticsJob() {
<-api.Srv.Store.System().Update(systemSecurityLastTime)
}
+ if ucr := <-api.Srv.Store.User().GetTotalUsersCount(); ucr.Err == nil {
+ v.Set(utils.PROP_DIAGNOSTIC_USER_COUNT, strconv.FormatInt(ucr.Data.(int64), 10))
+ }
+
+ if ucr := <-api.Srv.Store.User().GetTotalActiveUsersCount(); ucr.Err == nil {
+ v.Set(utils.PROP_DIAGNOSTIC_ACTIVE_USER_COUNT, strconv.FormatInt(ucr.Data.(int64), 10))
+ }
+
res, err := http.Get(utils.DIAGNOSTIC_URL + "/security?" + v.Encode())
if err != nil {
l4g.Error("Failed to get security update information from Mattermost.")
diff --git a/model/client.go b/model/client.go
index 11beb9a87..77b0aaad2 100644
--- a/model/client.go
+++ b/model/client.go
@@ -844,6 +844,32 @@ func (c *Client) ListIncomingWebhooks() (*Result, *AppError) {
}
}
+func (c *Client) SetPreferences(preferences *Preferences) (*Result, *AppError) {
+ if r, err := c.DoApiPost("/preferences/save", preferences.ToJson()); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), preferences}, nil
+ }
+}
+
+func (c *Client) GetPreference(category string, name string) (*Result, *AppError) {
+ if r, err := c.DoApiGet("/preferences/"+category+"/"+name, "", ""); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID), r.Header.Get(HEADER_ETAG_SERVER), PreferenceFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) GetPreferenceCategory(category string) (*Result, *AppError) {
+ if r, err := c.DoApiGet("/preferences/"+category, "", ""); 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) MockSession(sessionToken string) {
c.AuthToken = sessionToken
c.AuthType = HEADER_BEARER
diff --git a/model/config.go b/model/config.go
index e4b99ad4a..8a11b7bb7 100644
--- a/model/config.go
+++ b/model/config.go
@@ -32,6 +32,7 @@ type ServiceSettings struct {
EnablePostUsernameOverride bool
EnablePostIconOverride bool
EnableTesting bool
+ EnableSecurityFixAlert *bool
}
type SSOSettings struct {
@@ -110,9 +111,8 @@ type RateLimitSettings struct {
}
type PrivacySettings struct {
- ShowEmailAddress bool
- ShowFullName bool
- EnableSecurityFixAlert bool
+ ShowEmailAddress bool
+ ShowFullName bool
}
type TeamSettings struct {
@@ -163,6 +163,13 @@ func ConfigFromJson(data io.Reader) *Config {
}
}
+func (o *Config) SetDefaults() {
+ if o.ServiceSettings.EnableSecurityFixAlert == nil {
+ o.ServiceSettings.EnableSecurityFixAlert = new(bool)
+ *o.ServiceSettings.EnableSecurityFixAlert = true
+ }
+}
+
func (o *Config) IsValid() *AppError {
if o.ServiceSettings.MaximumLoginAttempts <= 0 {
diff --git a/model/preference.go b/model/preference.go
new file mode 100644
index 000000000..44279f71a
--- /dev/null
+++ b/model/preference.go
@@ -0,0 +1,60 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+const (
+ PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW = "direct_channel_show"
+)
+
+type Preference struct {
+ UserId string `json:"user_id"`
+ Category string `json:"category"`
+ Name string `json:"name"`
+ Value string `json:"value"`
+}
+
+func (o *Preference) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func PreferenceFromJson(data io.Reader) *Preference {
+ decoder := json.NewDecoder(data)
+ var o Preference
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
+
+func (o *Preference) IsValid() *AppError {
+ if len(o.UserId) != 26 {
+ return NewAppError("Preference.IsValid", "Invalid user id", "user_id="+o.UserId)
+ }
+
+ if len(o.Category) == 0 || len(o.Category) > 32 {
+ return NewAppError("Preference.IsValid", "Invalid category", "category="+o.Category)
+ }
+
+ if len(o.Name) == 0 || len(o.Name) > 32 {
+ return NewAppError("Preference.IsValid", "Invalid name", "name="+o.Name)
+ }
+
+ if len(o.Value) > 128 {
+ return NewAppError("Preference.IsValid", "Value is too long", "value="+o.Value)
+ }
+
+ return nil
+}
diff --git a/model/preference_test.go b/model/preference_test.go
new file mode 100644
index 000000000..66b7ac50b
--- /dev/null
+++ b/model/preference_test.go
@@ -0,0 +1,56 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestPreferenceIsValid(t *testing.T) {
+ preference := Preference{
+ UserId: "1234garbage",
+ Category: PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
+ Name: NewId(),
+ }
+
+ if err := preference.IsValid(); err == nil {
+ t.Fatal()
+ }
+
+ preference.UserId = NewId()
+ if err := preference.IsValid(); err != nil {
+ t.Fatal(err)
+ }
+
+ preference.Category = strings.Repeat("01234567890", 20)
+ if err := preference.IsValid(); err == nil {
+ t.Fatal()
+ }
+
+ preference.Category = PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW
+ if err := preference.IsValid(); err != nil {
+ t.Fatal()
+ }
+
+ preference.Name = strings.Repeat("01234567890", 20)
+ if err := preference.IsValid(); err == nil {
+ t.Fatal()
+ }
+
+ preference.Name = NewId()
+ if err := preference.IsValid(); err != nil {
+ t.Fatal()
+ }
+
+ preference.Value = strings.Repeat("01234567890", 20)
+ if err := preference.IsValid(); err == nil {
+ t.Fatal()
+ }
+
+ preference.Value = "1234garbage"
+ if err := preference.IsValid(); err != nil {
+ t.Fatal()
+ }
+}
diff --git a/model/preferences.go b/model/preferences.go
new file mode 100644
index 000000000..1ef16151f
--- /dev/null
+++ b/model/preferences.go
@@ -0,0 +1,31 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+type Preferences []Preference
+
+func (o *Preferences) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func PreferencesFromJson(data io.Reader) (Preferences, error) {
+ decoder := json.NewDecoder(data)
+ var o Preferences
+ err := decoder.Decode(&o)
+ if err == nil {
+ return o, nil
+ } else {
+ return nil, err
+ }
+}
diff --git a/model/version.go b/model/version.go
index d03f64ba2..a61004fde 100644
--- a/model/version.go
+++ b/model/version.go
@@ -12,6 +12,7 @@ import (
// It should be maitained in chronological order with most current
// release at the front of the list.
var versions = []string{
+ "1.1.0",
"1.0.0",
"0.7.1",
"0.7.0",
diff --git a/store/sql_preference_store.go b/store/sql_preference_store.go
new file mode 100644
index 000000000..46cef38b1
--- /dev/null
+++ b/store/sql_preference_store.go
@@ -0,0 +1,214 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ "github.com/go-gorp/gorp"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+type SqlPreferenceStore struct {
+ *SqlStore
+}
+
+func NewSqlPreferenceStore(sqlStore *SqlStore) PreferenceStore {
+ s := &SqlPreferenceStore{sqlStore}
+
+ for _, db := range sqlStore.GetAllConns() {
+ table := db.AddTableWithName(model.Preference{}, "Preferences").SetKeys(false, "UserId", "Category", "Name")
+ table.ColMap("UserId").SetMaxSize(26)
+ table.ColMap("Category").SetMaxSize(32)
+ table.ColMap("Name").SetMaxSize(32)
+ table.ColMap("Value").SetMaxSize(128)
+ }
+
+ return s
+}
+
+func (s SqlPreferenceStore) UpgradeSchemaIfNeeded() {
+}
+
+func (s SqlPreferenceStore) CreateIndexesIfNotExists() {
+ s.CreateIndexIfNotExists("idx_preferences_user_id", "Preferences", "UserId")
+ s.CreateIndexIfNotExists("idx_preferences_category", "Preferences", "Category")
+ s.CreateIndexIfNotExists("idx_preferences_name", "Preferences", "Name")
+}
+
+func (s SqlPreferenceStore) Save(preferences *model.Preferences) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ // wrap in a transaction so that if one fails, everything fails
+ transaction, err := s.GetReplica().Begin()
+ if err != nil {
+ result.Err = model.NewAppError("SqlPreferenceStore.Save", "Unable to open transaction to save preferences", err.Error())
+ } else {
+ for _, preference := range *preferences {
+ if upsertResult := s.save(transaction, &preference); upsertResult.Err != nil {
+ result = upsertResult
+ break
+ }
+ }
+
+ if result.Err == nil {
+ if err := transaction.Commit(); err != nil {
+ // don't need to rollback here since the transaction is already closed
+ result.Err = model.NewAppError("SqlPreferenceStore.Save", "Unable to commit transaction to save preferences", err.Error())
+ } else {
+ result.Data = len(*preferences)
+ }
+ } else {
+ if err := transaction.Rollback(); err != nil {
+ result.Err = model.NewAppError("SqlPreferenceStore.Save", "Unable to rollback transaction to save preferences", err.Error())
+ }
+ }
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlPreferenceStore) save(transaction *gorp.Transaction, preference *model.Preference) StoreResult {
+ result := StoreResult{}
+
+ if result.Err = preference.IsValid(); result.Err != nil {
+ return result
+ }
+
+ params := map[string]interface{}{
+ "UserId": preference.UserId,
+ "Category": preference.Category,
+ "Name": preference.Name,
+ "Value": preference.Value,
+ }
+
+ if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL {
+ if _, err := transaction.Exec(
+ `INSERT INTO
+ Preferences
+ (UserId, Category, Name, Value)
+ VALUES
+ (:UserId, :Category, :Name, :Value)
+ ON DUPLICATE KEY UPDATE
+ Value = :Value`, params); err != nil {
+ result.Err = model.NewAppError("SqlPreferenceStore.save", "We encountered an error while updating preferences", err.Error())
+ }
+ } else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
+ // postgres has no way to upsert values until version 9.5 and trying inserting and then updating causes transactions to abort
+ count, err := transaction.SelectInt(
+ `SELECT
+ count(0)
+ FROM
+ Preferences
+ WHERE
+ UserId = :UserId
+ AND Category = :Category
+ AND Name = :Name`, params)
+ if err != nil {
+ result.Err = model.NewAppError("SqlPreferenceStore.save", "We encountered an error while updating preferences", err.Error())
+ return result
+ }
+
+ if count == 1 {
+ s.update(transaction, preference)
+ } else {
+ s.insert(transaction, preference)
+ }
+ } else {
+ result.Err = model.NewAppError("SqlPreferenceStore.save", "We encountered an error while updating preferences",
+ "Failed to update preference because of missing driver")
+ }
+
+ return result
+}
+
+func (s SqlPreferenceStore) insert(transaction *gorp.Transaction, preference *model.Preference) StoreResult {
+ result := StoreResult{}
+
+ if err := transaction.Insert(preference); err != nil {
+ if IsUniqueConstraintError(err.Error(), "UserId", "preferences_pkey") {
+ result.Err = model.NewAppError("SqlPreferenceStore.insert", "A preference with that user id, category, and name already exists",
+ "user_id="+preference.UserId+", category="+preference.Category+", name="+preference.Name+", "+err.Error())
+ } else {
+ result.Err = model.NewAppError("SqlPreferenceStore.insert", "We couldn't save the preference",
+ "user_id="+preference.UserId+", category="+preference.Category+", name="+preference.Name+", "+err.Error())
+ }
+ }
+
+ return result
+}
+
+func (s SqlPreferenceStore) update(transaction *gorp.Transaction, preference *model.Preference) StoreResult {
+ result := StoreResult{}
+
+ if _, err := transaction.Update(preference); err != nil {
+ result.Err = model.NewAppError("SqlPreferenceStore.update", "We couldn't update the preference",
+ "user_id="+preference.UserId+", category="+preference.Category+", name="+preference.Name+", "+err.Error())
+ }
+
+ return result
+}
+
+func (s SqlPreferenceStore) Get(userId string, category string, name string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var preference model.Preference
+
+ if err := s.GetReplica().SelectOne(&preference,
+ `SELECT
+ *
+ FROM
+ Preferences
+ WHERE
+ UserId = :UserId
+ AND Category = :Category
+ AND Name = :Name`, map[string]interface{}{"UserId": userId, "Category": category, "Name": name}); err != nil {
+ result.Err = model.NewAppError("SqlPreferenceStore.Get", "We encounted an error while finding preferences", err.Error())
+ } else {
+ result.Data = preference
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlPreferenceStore) GetCategory(userId string, category 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
+ AND Category = :Category`, map[string]interface{}{"UserId": userId, "Category": category}); err != nil {
+ result.Err = model.NewAppError("SqlPreferenceStore.GetCategory", "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
new file mode 100644
index 000000000..76b1bcb17
--- /dev/null
+++ b/store/sql_preference_store_test.go
@@ -0,0 +1,146 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ "github.com/mattermost/platform/model"
+ "testing"
+)
+
+func TestPreferenceSave(t *testing.T) {
+ Setup()
+
+ id := model.NewId()
+
+ preferences := model.Preferences{
+ {
+ UserId: id,
+ Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
+ Name: model.NewId(),
+ Value: "value1a",
+ },
+ {
+ UserId: id,
+ Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
+ Name: model.NewId(),
+ Value: "value1b",
+ },
+ }
+ if count := Must(store.Preference().Save(&preferences)); count != 2 {
+ t.Fatal("got incorrect number of rows saved")
+ }
+
+ for _, preference := range preferences {
+ if data := Must(store.Preference().Get(preference.UserId, preference.Category, preference.Name)).(model.Preference); preference != data {
+ t.Fatal("got incorrect preference after first Save")
+ }
+ }
+
+ preferences[0].Value = "value2a"
+ preferences[1].Value = "value2b"
+ if count := Must(store.Preference().Save(&preferences)); count != 2 {
+ t.Fatal("got incorrect number of rows saved")
+ }
+
+ for _, preference := range preferences {
+ if data := Must(store.Preference().Get(preference.UserId, preference.Category, preference.Name)).(model.Preference); preference != data {
+ t.Fatal("got incorrect preference after second Save")
+ }
+ }
+}
+
+func TestPreferenceGet(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,
+ },
+ {
+ UserId: userId,
+ Category: category,
+ Name: model.NewId(),
+ },
+ {
+ UserId: userId,
+ Category: model.NewId(),
+ Name: name,
+ },
+ {
+ UserId: model.NewId(),
+ Category: category,
+ Name: name,
+ },
+ }
+
+ Must(store.Preference().Save(&preferences))
+
+ if result := <-store.Preference().Get(userId, category, name); result.Err != nil {
+ t.Fatal(result.Err)
+ } else if data := result.Data.(model.Preference); data != preferences[0] {
+ t.Fatal("got incorrect preference")
+ }
+
+ // make sure getting a missing preference fails
+ if result := <-store.Preference().Get(model.NewId(), model.NewId(), model.NewId()); result.Err == nil {
+ t.Fatal("no error on getting a missing preference")
+ }
+}
+
+func TestPreferenceGetCategory(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().GetCategory(userId, category); result.Err != nil {
+ t.Fatal(result.Err)
+ } else if data := result.Data.(model.Preferences); len(data) != 2 {
+ t.Fatal("got the wrong number of preferences")
+ } else if !((data[0] == preferences[0] && data[1] == preferences[1]) || (data[0] == preferences[1] && data[1] == preferences[0])) {
+ t.Fatal("got incorrect preferences")
+ }
+
+ // make sure getting a missing preference category doesn't fail
+ if result := <-store.Preference().GetCategory(model.NewId(), model.NewId()); result.Err != nil {
+ t.Fatal(result.Err)
+ } else if data := result.Data.(model.Preferences); len(data) != 0 {
+ t.Fatal("shouldn't have got any preferences")
+ }
+}
diff --git a/store/sql_store.go b/store/sql_store.go
index 900543460..4b055e455 100644
--- a/store/sql_store.go
+++ b/store/sql_store.go
@@ -30,17 +30,18 @@ import (
)
type SqlStore struct {
- master *gorp.DbMap
- replicas []*gorp.DbMap
- team TeamStore
- channel ChannelStore
- post PostStore
- user UserStore
- audit AuditStore
- session SessionStore
- oauth OAuthStore
- system SystemStore
- webhook WebhookStore
+ master *gorp.DbMap
+ replicas []*gorp.DbMap
+ team TeamStore
+ channel ChannelStore
+ post PostStore
+ user UserStore
+ audit AuditStore
+ session SessionStore
+ oauth OAuthStore
+ system SystemStore
+ webhook WebhookStore
+ preference PreferenceStore
}
func NewSqlStore() Store {
@@ -93,6 +94,7 @@ func NewSqlStore() Store {
sqlStore.oauth = NewSqlOAuthStore(sqlStore)
sqlStore.system = NewSqlSystemStore(sqlStore)
sqlStore.webhook = NewSqlWebhookStore(sqlStore)
+ sqlStore.preference = NewSqlPreferenceStore(sqlStore)
sqlStore.master.CreateTablesIfNotExists()
@@ -105,6 +107,7 @@ func NewSqlStore() Store {
sqlStore.oauth.(*SqlOAuthStore).UpgradeSchemaIfNeeded()
sqlStore.system.(*SqlSystemStore).UpgradeSchemaIfNeeded()
sqlStore.webhook.(*SqlWebhookStore).UpgradeSchemaIfNeeded()
+ sqlStore.preference.(*SqlPreferenceStore).UpgradeSchemaIfNeeded()
sqlStore.team.(*SqlTeamStore).CreateIndexesIfNotExists()
sqlStore.channel.(*SqlChannelStore).CreateIndexesIfNotExists()
@@ -115,6 +118,7 @@ func NewSqlStore() Store {
sqlStore.oauth.(*SqlOAuthStore).CreateIndexesIfNotExists()
sqlStore.system.(*SqlSystemStore).CreateIndexesIfNotExists()
sqlStore.webhook.(*SqlWebhookStore).CreateIndexesIfNotExists()
+ sqlStore.preference.(*SqlPreferenceStore).CreateIndexesIfNotExists()
if model.IsPreviousVersion(schemaVersion) {
sqlStore.system.Update(&model.System{Name: "Version", Value: model.CurrentVersion})
@@ -472,6 +476,10 @@ func (ss SqlStore) Webhook() WebhookStore {
return ss.webhook
}
+func (ss SqlStore) Preference() PreferenceStore {
+ return ss.preference
+}
+
type mattermConverter struct{}
func (me mattermConverter) ToDb(val interface{}) (interface{}, error) {
diff --git a/store/sql_user_store.go b/store/sql_user_store.go
index 011acd7e4..dc6b07a16 100644
--- a/store/sql_user_store.go
+++ b/store/sql_user_store.go
@@ -530,3 +530,24 @@ func (us SqlUserStore) GetTotalUsersCount() StoreChannel {
return storeChannel
}
+
+func (us SqlUserStore) GetTotalActiveUsersCount() StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ time := model.GetMillis() - (1000 * 60 * 60 * 12)
+
+ if count, err := us.GetReplica().SelectInt("SELECT COUNT(Id) FROM Users WHERE LastActivityAt > :Time", map[string]interface{}{"Time": time}); err != nil {
+ result.Err = model.NewAppError("SqlUserStore.GetTotalActiveUsersCount", "We could not count the users", err.Error())
+ } else {
+ result.Data = count
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_user_store_test.go b/store/sql_user_store_test.go
index be21c8bd2..874baf634 100644
--- a/store/sql_user_store_test.go
+++ b/store/sql_user_store_test.go
@@ -206,7 +206,7 @@ func TestUserStoreGet(t *testing.T) {
}
}
-func TestUserCountt(t *testing.T) {
+func TestUserCount(t *testing.T) {
Setup()
u1 := model.User{}
@@ -224,6 +224,24 @@ func TestUserCountt(t *testing.T) {
}
}
+func TestActiveUserCount(t *testing.T) {
+ Setup()
+
+ u1 := model.User{}
+ u1.TeamId = model.NewId()
+ u1.Email = model.NewId()
+ Must(store.User().Save(&u1))
+
+ if result := <-store.User().GetTotalActiveUsersCount(); result.Err != nil {
+ t.Fatal(result.Err)
+ } else {
+ count := result.Data.(int64)
+ if count <= 0 {
+ t.Fatal()
+ }
+ }
+}
+
func TestUserStoreGetProfiles(t *testing.T) {
Setup()
diff --git a/store/store.go b/store/store.go
index 1c4d08e36..6e1614ccb 100644
--- a/store/store.go
+++ b/store/store.go
@@ -37,6 +37,7 @@ type Store interface {
OAuth() OAuthStore
System() SystemStore
Webhook() WebhookStore
+ Preference() PreferenceStore
Close()
}
@@ -104,6 +105,7 @@ type UserStore interface {
UpdateFailedPasswordAttempts(userId string, attempts int) StoreChannel
GetForExport(teamId string) StoreChannel
GetTotalUsersCount() StoreChannel
+ GetTotalActiveUsersCount() StoreChannel
GetSystemAdminProfiles() StoreChannel
}
@@ -148,3 +150,9 @@ type WebhookStore interface {
GetIncomingByUser(userId string) StoreChannel
DeleteIncoming(webhookId string, time int64) StoreChannel
}
+
+type PreferenceStore interface {
+ Save(preferences *model.Preferences) StoreChannel
+ Get(userId string, category string, name string) StoreChannel
+ GetCategory(userId string, category string) StoreChannel
+}
diff --git a/utils/config.go b/utils/config.go
index 44ee14a6e..2c6f30bf0 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -150,6 +150,8 @@ func LoadConfig(fileName string) {
CfgFileName = fileName
}
+ config.SetDefaults()
+
if err := config.IsValid(); err != nil {
panic("Error validating config file=" + fileName + ", err=" + err.Message)
}
diff --git a/utils/diagnostic.go b/utils/diagnostic.go
index da02e771b..8572c2f51 100644
--- a/utils/diagnostic.go
+++ b/utils/diagnostic.go
@@ -6,24 +6,23 @@ package utils
import (
"net/http"
"net/url"
-
- "github.com/mattermost/platform/model"
)
const (
DIAGNOSTIC_URL = "https://d7zmvsa9e04kk.cloudfront.net"
- PROP_DIAGNOSTIC_ID = "id"
- PROP_DIAGNOSTIC_CATEGORY = "c"
- VAL_DIAGNOSTIC_CATEGORY_DEFAULT = "d"
- PROP_DIAGNOSTIC_BUILD = "b"
- PROP_DIAGNOSTIC_DATABASE = "db"
- PROP_DIAGNOSTIC_OS = "os"
- PROP_DIAGNOSTIC_USER_COUNT = "uc"
+ PROP_DIAGNOSTIC_ID = "id"
+ PROP_DIAGNOSTIC_CATEGORY = "c"
+ VAL_DIAGNOSTIC_CATEGORY_DEFAULT = "d"
+ PROP_DIAGNOSTIC_BUILD = "b"
+ PROP_DIAGNOSTIC_DATABASE = "db"
+ PROP_DIAGNOSTIC_OS = "os"
+ PROP_DIAGNOSTIC_USER_COUNT = "uc"
+ PROP_DIAGNOSTIC_ACTIVE_USER_COUNT = "auc"
)
func SendDiagnostic(values url.Values) {
- if Cfg.PrivacySettings.EnableSecurityFixAlert && model.IsOfficalBuild() {
+ if *Cfg.ServiceSettings.EnableSecurityFixAlert {
res, err := http.Get(DIAGNOSTIC_URL + "/i?" + values.Encode())
if err != nil {
diff --git a/web/react/components/admin_console/gitlab_settings.jsx b/web/react/components/admin_console/gitlab_settings.jsx
index 6b97287e4..5c22bf5cf 100644
--- a/web/react/components/admin_console/gitlab_settings.jsx
+++ b/web/react/components/admin_console/gitlab_settings.jsx
@@ -113,7 +113,15 @@ export default class GitLabSettings extends React.Component {
/>
{'false'}
</label>
- <p className='help-text'>{'When true, Mattermost allows team creation and account signup using GitLab OAuth. To configure, log in to your GitLab account and go to Applications -> Profile Settings. Enter Redirect URIs "<your-mattermost-url>/login/gitlab/complete" (example: http://localhost:8065/login/gitlab/complete) and "<your-mattermost-url>/signup/gitlab/complete". Then use "Secret" and "Id" fields to complete the options below.'}</p>
+ <p className='help-text'>
+ {'When true, Mattermost allows team creation and account signup using GitLab OAuth.'} <br/>
+ </p>
+ <ol className='help-text'>
+ <li>{'Log in to your GitLab account and go to Applications -> Profile Settings.'}</li>
+ <li>{'Enter Redirect URIs "<your-mattermost-url>/login/gitlab/complete" (example: http://localhost:8065/login/gitlab/complete) and "<your-mattermost-url>/signup/gitlab/complete". '}</li>
+ <li>{'Then use "Secret" and "Id" fields from GitLab to complete the options below.'}</li>
+ <li>{'Complete the Endpoint URLs below. '}</li>
+ </ol>
</div>
</div>
@@ -179,7 +187,7 @@ export default class GitLabSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.Enable}
/>
- <p className='help-text'>{'Enter <your-gitlab-url>/oauth/authorize (example http://localhost:3000/oauth/authorize). Make sure you use HTTP or HTTPS in your URLs as appropriate.'}</p>
+ <p className='help-text'>{'Enter https://<your-gitlab-url>/oauth/authorize (example https://example.com:3000/oauth/authorize). Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'}</p>
</div>
</div>
@@ -201,7 +209,7 @@ export default class GitLabSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.Enable}
/>
- <p className='help-text'>{'Enter <your-gitlab-url>/oauth/token. Make sure you use HTTP or HTTPS in your URLs as appropriate.'}</p>
+ <p className='help-text'>{'Enter https://<your-gitlab-url>/oauth/token. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'}</p>
</div>
</div>
@@ -223,7 +231,7 @@ export default class GitLabSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.Enable}
/>
- <p className='help-text'>{'Enter <your-gitlab-url>/api/v3/user. Make sure you use HTTP or HTTPS in your URLs as appropriate.'}</p>
+ <p className='help-text'>{'Enter https://<your-gitlab-url>/api/v3/user. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'}</p>
</div>
</div>
diff --git a/web/react/components/admin_console/privacy_settings.jsx b/web/react/components/admin_console/privacy_settings.jsx
index a32ca3136..70ec04f4a 100644
--- a/web/react/components/admin_console/privacy_settings.jsx
+++ b/web/react/components/admin_console/privacy_settings.jsx
@@ -30,7 +30,6 @@ export default class PrivacySettings extends React.Component {
var config = this.props.config;
config.PrivacySettings.ShowEmailAddress = React.findDOMNode(this.refs.ShowEmailAddress).checked;
config.PrivacySettings.ShowFullName = React.findDOMNode(this.refs.ShowFullName).checked;
- config.PrivacySettings.EnableSecurityFixAlert = React.findDOMNode(this.refs.EnableSecurityFixAlert).checked;
Client.saveConfig(
config,
@@ -138,39 +137,6 @@ export default class PrivacySettings extends React.Component {
</div>
<div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableSecurityFixAlert'
- >
- {'Send Error and Diagnostic: '}
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableSecurityFixAlert'
- value='true'
- ref='EnableSecurityFixAlert'
- defaultChecked={this.props.config.PrivacySettings.EnableSecurityFixAlert}
- onChange={this.handleChange}
- />
- {'true'}
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableSecurityFixAlert'
- value='false'
- defaultChecked={!this.props.config.PrivacySettings.EnableSecurityFixAlert}
- onChange={this.handleChange}
- />
- {'false'}
- </label>
- <p className='help-text'>{'When true, System Administrators are notified by email if a relevant security fix alert has been announced in the last 12 hours. Requires email to be enabled.'}</p>
- </div>
- </div>
-
- <div className='form-group'>
<div className='col-sm-12'>
{serverError}
<button
diff --git a/web/react/components/admin_console/service_settings.jsx b/web/react/components/admin_console/service_settings.jsx
index 3968d9820..f29d62646 100644
--- a/web/react/components/admin_console/service_settings.jsx
+++ b/web/react/components/admin_console/service_settings.jsx
@@ -35,11 +35,13 @@ export default class ServiceSettings extends React.Component {
config.ServiceSettings.SegmentDeveloperKey = React.findDOMNode(this.refs.SegmentDeveloperKey).value.trim();
config.ServiceSettings.GoogleDeveloperKey = React.findDOMNode(this.refs.GoogleDeveloperKey).value.trim();
- //config.ServiceSettings.EnableOAuthServiceProvider = React.findDOMNode(this.refs.EnableOAuthServiceProvider).checked;
config.ServiceSettings.EnableIncomingWebhooks = React.findDOMNode(this.refs.EnableIncomingWebhooks).checked;
config.ServiceSettings.EnablePostUsernameOverride = React.findDOMNode(this.refs.EnablePostUsernameOverride).checked;
config.ServiceSettings.EnablePostIconOverride = React.findDOMNode(this.refs.EnablePostIconOverride).checked;
config.ServiceSettings.EnableTesting = React.findDOMNode(this.refs.EnableTesting).checked;
+ config.ServiceSettings.EnableSecurityFixAlert = React.findDOMNode(this.refs.EnableSecurityFixAlert).checked;
+
+ //config.ServiceSettings.EnableOAuthServiceProvider = React.findDOMNode(this.refs.EnableOAuthServiceProvider).checked;
var MaximumLoginAttempts = 10;
if (!isNaN(parseInt(React.findDOMNode(this.refs.MaximumLoginAttempts).value, 10))) {
@@ -305,6 +307,39 @@ export default class ServiceSettings extends React.Component {
</div>
<div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableSecurityFixAlert'
+ >
+ {'Enable Security Alerts: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableSecurityFixAlert'
+ value='true'
+ ref='EnableSecurityFixAlert'
+ defaultChecked={this.props.config.ServiceSettings.EnableSecurityFixAlert}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableSecurityFixAlert'
+ value='false'
+ defaultChecked={!this.props.config.ServiceSettings.EnableSecurityFixAlert}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, System Administrators are notified by email if a relevant security fix alert has been announced in the last 12 hours. Requires email to be enabled.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
<div className='col-sm-12'>
{serverError}
<button
diff --git a/web/react/components/admin_console/user_item.jsx b/web/react/components/admin_console/user_item.jsx
index 665ccd719..395e22e6c 100644
--- a/web/react/components/admin_console/user_item.jsx
+++ b/web/react/components/admin_console/user_item.jsx
@@ -111,8 +111,10 @@ export default class UserItem extends React.Component {
const user = this.props.user;
let currentRoles = 'Member';
if (user.roles.length > 0) {
- if (user.roles.indexOf('system_admin') > -1) {
+ if (Utils.isSystemAdmin(user.roles)) {
currentRoles = 'System Admin';
+ } else if (Utils.isAdmin(user.roles)) {
+ currentRoles = 'Team Admin';
} else {
currentRoles = user.roles.charAt(0).toUpperCase() + user.roles.slice(1);
}
@@ -158,7 +160,7 @@ export default class UserItem extends React.Component {
href='#'
onClick={this.handleMakeAdmin}
>
- {'Make Admin'}
+ {'Make Team Admin'}
</a>
</li>
);
diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx
index d16069725..d0d6ab5e2 100644
--- a/web/react/components/channel_loader.jsx
+++ b/web/react/components/channel_loader.jsx
@@ -104,12 +104,6 @@ export default class ChannelLoader extends React.Component {
}
});
- /* Setup modal events */
- $('.modal').on('show.bs.modal', function onShow() {
- $('.modal-body').css('overflow-y', 'auto');
- $('.modal-body').css('max-height', $(window).height() * 0.7);
- });
-
/* Prevent backspace from navigating back a page */
$(window).on('keydown.preventBackspace', (e) => {
if (e.which === 8 && !$(e.target).is('input, textarea')) {
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index add4125d7..680d693f1 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -262,25 +262,27 @@ export default class CreateComment extends React.Component {
id={this.props.rootId}
className='post-create-body comment-create-body'
>
- <Textbox
- onUserInput={this.handleUserInput}
- onKeyPress={this.commentMsgKeyPress}
- messageText={this.state.messageText}
- createMessage='Add a comment...'
- initialText=''
- id='reply_textbox'
- ref='textbox'
- />
- <FileUpload
- ref='fileUpload'
- getFileCount={this.getFileCount}
- onUploadStart={this.handleUploadStart}
- onFileUpload={this.handleFileUploadComplete}
- onUploadError={this.handleUploadError}
- onTextDrop={this.handleTextDrop}
- postType='comment'
- channelId={this.props.channelId}
- />
+ <div className='post-body__cell'>
+ <Textbox
+ onUserInput={this.handleUserInput}
+ onKeyPress={this.commentMsgKeyPress}
+ messageText={this.state.messageText}
+ createMessage='Add a comment...'
+ initialText=''
+ id='reply_textbox'
+ ref='textbox'
+ />
+ <FileUpload
+ ref='fileUpload'
+ getFileCount={this.getFileCount}
+ onUploadStart={this.handleUploadStart}
+ onFileUpload={this.handleFileUploadComplete}
+ onUploadError={this.handleUploadError}
+ onTextDrop={this.handleTextDrop}
+ postType='comment'
+ channelId={this.props.channelId}
+ />
+ </div>
</div>
<MsgTyping
channelId={this.props.channelId}
diff --git a/web/react/components/email_verify.jsx b/web/react/components/email_verify.jsx
index 2b413b848..9be7f97f8 100644
--- a/web/react/components/email_verify.jsx
+++ b/web/react/components/email_verify.jsx
@@ -22,7 +22,7 @@ export default class EmailVerify extends React.Component {
title = global.window.config.SiteName + ' Email Verified';
body = <p>Your email has been verified! Click <a href={this.props.teamURL + '?email=' + this.props.userEmail}>here</a> to log in.</p>;
} else {
- title = global.window.config.SiteName + ' Email Not Verified';
+ title = global.window.config.SiteName + ': You are almost done';
body = <p>Please verify your email address. Check your inbox for an email.</p>;
resend = (
<button
diff --git a/web/react/components/find_team.jsx b/web/react/components/find_team.jsx
index 59c4e08e3..9e3e3a683 100644
--- a/web/react/components/find_team.jsx
+++ b/web/react/components/find_team.jsx
@@ -70,6 +70,7 @@ export default class FindTeam extends React.Component {
className='form-control'
placeholder='you@domain.com'
maxLength='128'
+ spellCheck='false'
/>
{emailError}
</div>
diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx
index 234013b93..eb6bfa9b6 100644
--- a/web/react/components/get_link_modal.jsx
+++ b/web/react/components/get_link_modal.jsx
@@ -96,7 +96,6 @@ export default class GetLinkModal extends React.Component {
<p>
Send teammates the link below for them to sign-up to this team site.
<br /><br />
- Be careful not to share this link publicly, since anyone with the link can join your team.
</p>
<textarea
className='form-control no-resize'
diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx
index 2ca39d1b1..c2f2c15ac 100644
--- a/web/react/components/invite_member_modal.jsx
+++ b/web/react/components/invite_member_modal.jsx
@@ -211,6 +211,7 @@ export default class InviteMemberModal extends React.Component {
placeholder='First name'
maxLength='64'
disabled={!this.state.emailEnabled}
+ spellCheck='false'
/>
{firstNameError}
</div>
@@ -224,6 +225,7 @@ export default class InviteMemberModal extends React.Component {
placeholder='Last name'
maxLength='64'
disabled={!this.state.emailEnabled}
+ spellCheck='false'
/>
{lastNameError}
</div>
@@ -242,6 +244,7 @@ export default class InviteMemberModal extends React.Component {
placeholder='email@domain.com'
maxLength='64'
disabled={!this.state.emailEnabled}
+ spellCheck='false'
/>
{emailError}
</div>
diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx
index 997abce68..f81822e1e 100644
--- a/web/react/components/login.jsx
+++ b/web/react/components/login.jsx
@@ -136,6 +136,7 @@ export default class Login extends React.Component {
defaultValue={priorEmail}
ref='email'
placeholder='Email'
+ spellCheck='false'
/>
</div>
<div className={'form-group' + errorClass}>
@@ -146,6 +147,7 @@ export default class Login extends React.Component {
name='password'
ref='password'
placeholder='Password'
+ spellCheck='false'
/>
</div>
<div className='form-group'>
diff --git a/web/react/components/member_list_team_item.jsx b/web/react/components/member_list_team_item.jsx
index 9a104aa32..3af1d3800 100644
--- a/web/react/components/member_list_team_item.jsx
+++ b/web/react/components/member_list_team_item.jsx
@@ -82,8 +82,10 @@ export default class MemberListTeamItem extends React.Component {
const timestamp = UserStore.getCurrentUser().update_at;
if (user.roles.length > 0) {
- if (user.roles.indexOf('system_admin') > -1) {
+ if (Utils.isSystemAdmin(user.roles)) {
currentRoles = 'System Admin';
+ } else if (Utils.isAdmin(user.roles)) {
+ currentRoles = 'Team Admin';
} else {
currentRoles = user.roles.charAt(0).toUpperCase() + user.roles.slice(1);
}
@@ -112,7 +114,7 @@ export default class MemberListTeamItem extends React.Component {
href='#'
onClick={this.handleMakeAdmin}
>
- {'Make Admin'}
+ {'Make Team Admin'}
</a>
</li>
);
diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx
index 31ecb4c5d..bc610cd60 100644
--- a/web/react/components/more_direct_channels.jsx
+++ b/web/react/components/more_direct_channels.jsx
@@ -1,10 +1,11 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var ChannelStore = require('../stores/channel_store.jsx');
var TeamStore = require('../stores/team_store.jsx');
var Client = require('../utils/client.jsx');
+var Constants = require('../utils/constants.jsx');
var AsyncClient = require('../utils/async_client.jsx');
+var PreferenceStore = require('../stores/preference_store.jsx');
var utils = require('../utils/utils.jsx');
export default class MoreDirectChannels extends React.Component {
@@ -15,27 +16,28 @@ export default class MoreDirectChannels extends React.Component {
}
componentDidMount() {
- var self = this;
- $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function showModal(e) {
+ $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', (e) => {
var button = e.relatedTarget;
- self.setState({channels: $(button).data('channels')});
+ this.setState({channels: $(button).data('channels')}); // eslint-disable-line react/no-did-mount-set-state
});
}
- render() {
- var self = this;
+ handleJoinDirectChannel(channel) {
+ const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, channel.teammate_id, 'true');
+ AsyncClient.savePreferences([preference]);
+ }
- var directMessageItems = this.state.channels.map(function mapActivityToChannel(channel, index) {
+ render() {
+ var directMessageItems = this.state.channels.map((channel, index) => {
var badge = '';
var titleClass = '';
- var active = '';
var handleClick = null;
if (channel.fake) {
// It's a direct message channel that doesn't exist yet so let's create it now
var otherUserId = utils.getUserIdFromChannelName(channel);
- if (self.state.loadingDMChannel === index) {
+ if (this.state.loadingDMChannel === index) {
badge = (
<img
className='channel-loading-gif pull-right'
@@ -44,47 +46,42 @@ export default class MoreDirectChannels extends React.Component {
);
}
- if (self.state.loadingDMChannel === -1) {
- handleClick = function clickHandler(e) {
+ if (this.state.loadingDMChannel === -1) {
+ handleClick = (e) => {
e.preventDefault();
- self.setState({loadingDMChannel: index});
+ this.setState({loadingDMChannel: index});
+ this.handleJoinDirectChannel(channel);
Client.createDirectChannel(channel, otherUserId,
- function success(data) {
- $(React.findDOMNode(self.refs.modal)).modal('hide');
- self.setState({loadingDMChannel: -1});
+ (data) => {
+ $(React.findDOMNode(this.refs.modal)).modal('hide');
+ this.setState({loadingDMChannel: -1});
AsyncClient.getChannel(data.id);
utils.switchChannel(data);
},
- function error() {
- self.setState({loadingDMChannel: -1});
+ () => {
+ this.setState({loadingDMChannel: -1});
window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name;
}
);
};
}
} else {
- if (channel.id === ChannelStore.getCurrentId()) {
- active = 'active';
- }
-
if (channel.unread) {
badge = <span className='badge pull-right small'>{channel.unread}</span>;
titleClass = 'unread-title';
}
- handleClick = function clickHandler(e) {
+ handleClick = (e) => {
e.preventDefault();
+ this.handleJoinDirectChannel(channel);
utils.switchChannel(channel);
- $(React.findDOMNode(self.refs.modal)).modal('hide');
+ $(React.findDOMNode(this.refs.modal)).modal('hide');
};
}
return (
- <li
- key={channel.name}
- className={active}
- >
+ <li key={channel.name}>
<a
className={'sidebar-channel ' + titleClass}
href='#'
@@ -111,10 +108,10 @@ export default class MoreDirectChannels extends React.Component {
className='close'
data-dismiss='modal'
>
- <span aria-hidden='true'>&times;</span>
- <span className='sr-only'>Close</span>
+ <span aria-hidden='true'>{'×'}</span>
+ <span className='sr-only'>{'Close'}</span>
</button>
- <h4 className='modal-title'>More Direct Messages</h4>
+ <h4 className='modal-title'>{'More Direct Messages'}</h4>
</div>
<div className='modal-body'>
<ul className='nav nav-pills nav-stacked'>
@@ -126,7 +123,7 @@ export default class MoreDirectChannels extends React.Component {
type='button'
className='btn btn-default'
data-dismiss='modal'
- >Close</button>
+ >{'Close'}</button>
</div>
</div>
</div>
diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx
index b6defc393..49d517419 100644
--- a/web/react/components/navbar_dropdown.jsx
+++ b/web/react/components/navbar_dropdown.jsx
@@ -111,7 +111,7 @@ export default class NavbarDropdown extends React.Component {
data-toggle='modal'
data-target='#team_members'
>
- {'Manage Team'}
+ {'Manage Members'}
</a>
</li>
);
diff --git a/web/react/components/password_reset_form.jsx b/web/react/components/password_reset_form.jsx
index 0ef187114..6112adbaf 100644
--- a/web/react/components/password_reset_form.jsx
+++ b/web/react/components/password_reset_form.jsx
@@ -69,6 +69,7 @@ export default class PasswordResetForm extends React.Component {
name='password'
ref='password'
placeholder='Password'
+ spellCheck='false'
/>
</div>
{error}
diff --git a/web/react/components/password_reset_send_link.jsx b/web/react/components/password_reset_send_link.jsx
index 78fbcaa2f..f547499b0 100644
--- a/web/react/components/password_reset_send_link.jsx
+++ b/web/react/components/password_reset_send_link.jsx
@@ -73,6 +73,7 @@ export default class PasswordResetSendLink extends React.Component {
name='email'
ref='email'
placeholder='Email'
+ spellCheck='false'
/>
</div>
{error}
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index 87962641f..7addd825f 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -150,6 +150,7 @@ export default class PostInfo extends React.Component {
<ul className='post-header post-info'>
<li className='post-header-col'>
<OverlayTrigger
+ container={this}
placement='top'
overlay={tooltip}
>
diff --git a/web/react/components/setting_item_max.jsx b/web/react/components/setting_item_max.jsx
index d2cbc798e..4f0fe3ed0 100644
--- a/web/react/components/setting_item_max.jsx
+++ b/web/react/components/setting_item_max.jsx
@@ -32,11 +32,17 @@ export default class SettingItemMax extends React.Component {
}
var inputs = this.props.inputs;
+ var widthClass;
+ if (this.props.width === 'full') {
+ widthClass = 'col-sm-12';
+ } else {
+ widthClass = 'col-sm-9 col-sm-offset-3';
+ }
return (
<ul className='section-max form-horizontal'>
<li className='col-sm-12 section-title'>{this.props.title}</li>
- <li className='col-sm-9 col-sm-offset-3'>
+ <li className={widthClass}>
<ul className='setting-list'>
<li className='setting-list-item'>
{inputs}
@@ -69,5 +75,6 @@ SettingItemMax.propTypes = {
extraInfo: React.PropTypes.element,
updateSection: React.PropTypes.func,
submit: React.PropTypes.func,
- title: React.PropTypes.string
+ title: React.PropTypes.string,
+ width: React.PropTypes.string
};
diff --git a/web/react/components/settings_sidebar.jsx b/web/react/components/settings_sidebar.jsx
index b5d2132d7..66568e1c8 100644
--- a/web/react/components/settings_sidebar.jsx
+++ b/web/react/components/settings_sidebar.jsx
@@ -10,7 +10,7 @@ export default class SettingsSidebar extends React.Component {
handleClick(tab, e) {
e.preventDefault();
this.props.updateTab(tab.name);
- $('.settings-modal').addClass('display--content');
+ $(e.target).closest('.settings-modal').addClass('display--content');
}
render() {
let tabList = this.props.tabs.map(function makeTab(tab) {
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index 4ac1fd4a0..a1a5b64ba 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -1,19 +1,20 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var ChannelStore = require('../stores/channel_store.jsx');
-var Client = require('../utils/client.jsx');
-var AsyncClient = require('../utils/async_client.jsx');
-var SocketStore = require('../stores/socket_store.jsx');
-var UserStore = require('../stores/user_store.jsx');
-var TeamStore = require('../stores/team_store.jsx');
-var BrowserStore = require('../stores/browser_store.jsx');
-var Utils = require('../utils/utils.jsx');
-var SidebarHeader = require('./sidebar_header.jsx');
-var SearchBox = require('./search_bar.jsx');
-var Constants = require('../utils/constants.jsx');
-var NewChannelFlow = require('./new_channel_flow.jsx');
-var UnreadChannelIndicator = require('./unread_channel_indicator.jsx');
+const AsyncClient = require('../utils/async_client.jsx');
+const BrowserStore = require('../stores/browser_store.jsx');
+const ChannelStore = require('../stores/channel_store.jsx');
+const Client = require('../utils/client.jsx');
+const Constants = require('../utils/constants.jsx');
+const PreferenceStore = require('../stores/preference_store.jsx');
+const NewChannelFlow = require('./new_channel_flow.jsx');
+const SearchBox = require('./search_bar.jsx');
+const SidebarHeader = require('./sidebar_header.jsx');
+const SocketStore = require('../stores/socket_store.jsx');
+const TeamStore = require('../stores/team_store.jsx');
+const UnreadChannelIndicator = require('./unread_channel_indicator.jsx');
+const UserStore = require('../stores/user_store.jsx');
+const Utils = require('../utils/utils.jsx');
export default class Sidebar extends React.Component {
constructor(props) {
@@ -23,12 +24,17 @@ export default class Sidebar extends React.Component {
this.firstUnreadChannel = null;
this.lastUnreadChannel = null;
+ this.getStateFromStores = this.getStateFromStores.bind(this);
+
this.onChange = this.onChange.bind(this);
this.onScroll = this.onScroll.bind(this);
this.onResize = this.onResize.bind(this);
this.updateUnreadIndicators = this.updateUnreadIndicators.bind(this);
+ this.handleLeaveDirectChannel = this.handleLeaveDirectChannel.bind(this);
this.createChannelElement = this.createChannelElement.bind(this);
+ this.isLeaving = new Map();
+
const state = this.getStateFromStores();
state.modal = '';
state.loadingDMChannel = -1;
@@ -36,7 +42,7 @@ export default class Sidebar extends React.Component {
this.state = state;
}
getStateFromStores() {
- var members = ChannelStore.getAllMembers();
+ const members = ChannelStore.getAllMembers();
var teamMemberMap = UserStore.getActiveOnlyProfiles();
var currentId = ChannelStore.getCurrentId();
@@ -48,11 +54,13 @@ export default class Sidebar extends React.Component {
teammates.push(teamMemberMap[id]);
}
+ const preferences = PreferenceStore.getPreferences(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
+
// Create lists of all read and unread direct channels
- var showDirectChannels = [];
- var readDirectChannels = [];
+ var visibleDirectChannels = [];
+ var hiddenDirectChannels = [];
for (var i = 0; i < teammates.length; i++) {
- var teammate = teammates[i];
+ const teammate = teammates[i];
if (teammate.id === UserStore.getCurrentId()) {
continue;
@@ -65,90 +73,63 @@ export default class Sidebar extends React.Component {
channelName = teammate.id + '__' + UserStore.getCurrentId();
}
- var channel = ChannelStore.getByName(channelName);
-
- if (channel == null) {
- var tempChannel = {};
- tempChannel.fake = true;
- tempChannel.name = channelName;
- tempChannel.display_name = teammate.username;
- tempChannel.teammate_username = teammate.username;
- tempChannel.status = UserStore.getStatus(teammate.id);
- tempChannel.last_post_at = 0;
- tempChannel.total_msg_count = 0;
- tempChannel.type = 'D';
- readDirectChannels.push(tempChannel);
- } else {
- channel.display_name = teammate.username;
- channel.teammate_username = teammate.username;
+ let forceShow = false;
+ let channel = ChannelStore.getByName(channelName);
- channel.status = UserStore.getStatus(teammate.id);
+ if (channel) {
+ const member = members[channel.id];
+ const msgCount = channel.total_msg_count - member.msg_count;
- var channelMember = members[channel.id];
- var msgCount = channel.total_msg_count - channelMember.msg_count;
- if (msgCount > 0) {
- showDirectChannels.push(channel);
- } else if (currentId === channel.id) {
- showDirectChannels.push(channel);
- } else {
- readDirectChannels.push(channel);
- }
+ // always show a channel if either it is the current one or if it is unread, but it is not currently being left
+ forceShow = (currentId === channel.id || msgCount > 0) && !this.isLeaving.get(channel.id);
+ } else {
+ channel = {};
+ channel.fake = true;
+ channel.name = channelName;
+ channel.last_post_at = 0;
+ channel.total_msg_count = 0;
+ channel.type = 'D';
}
- }
- // If we don't have MAX_DMS unread channels, sort the read list by last_post_at
- if (showDirectChannels.length < Constants.MAX_DMS) {
- readDirectChannels.sort(function sortByLastPost(a, b) {
- // sort by last_post_at first
- if (a.last_post_at > b.last_post_at) {
- return -1;
- }
- if (a.last_post_at < b.last_post_at) {
- return 1;
- }
+ channel.display_name = teammate.username;
+ channel.teammate_id = teammate.id;
+ channel.status = UserStore.getStatus(teammate.id);
- // if last_post_at is equal, sort by name
- if (a.display_name < b.display_name) {
- return -1;
- }
- if (a.display_name > b.display_name) {
- return 1;
- }
- return 0;
- });
+ if (preferences.some((preference) => (preference.name === teammate.id && preference.value !== 'false'))) {
+ visibleDirectChannels.push(channel);
+ } else if (forceShow) {
+ // make sure that unread direct channels are visible
+ const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, teammate.id, 'true');
+ AsyncClient.savePreferences([preference]);
- var index = 0;
- while (showDirectChannels.length < Constants.MAX_DMS && index < readDirectChannels.length) {
- showDirectChannels.push(readDirectChannels[index]);
- index++;
+ visibleDirectChannels.push(channel);
+ } else {
+ hiddenDirectChannels.push(channel);
}
- readDirectChannels = readDirectChannels.slice(index);
-
- showDirectChannels.sort(function directSort(a, b) {
- if (a.display_name < b.display_name) {
- return -1;
- }
- if (a.display_name > b.display_name) {
- return 1;
- }
- return 0;
- });
}
+ visibleDirectChannels.sort(this.sortChannelsByDisplayName);
+ hiddenDirectChannels.sort(this.sortChannelsByDisplayName);
+
return {
activeId: currentId,
channels: ChannelStore.getAll(),
- members: members,
- showDirectChannels: showDirectChannels,
- hideDirectChannels: readDirectChannels
+ members,
+ visibleDirectChannels,
+ hiddenDirectChannels
};
}
+
componentDidMount() {
ChannelStore.addChangeListener(this.onChange);
UserStore.addChangeListener(this.onChange);
UserStore.addStatusesChangeListener(this.onChange);
TeamStore.addChangeListener(this.onChange);
SocketStore.addChangeListener(this.onSocketChange);
+ PreferenceStore.addChangeListener(this.onChange);
+
+ AsyncClient.getDirectChannelPreferences();
+
$('.nav-pills__container').perfectScrollbar();
this.updateTitle();
@@ -178,6 +159,7 @@ export default class Sidebar extends React.Component {
UserStore.removeStatusesChangeListener(this.onChange);
TeamStore.removeChangeListener(this.onChange);
SocketStore.removeChangeListener(this.onSocketChange);
+ PreferenceStore.removeChangeListener(this.onChange);
}
onChange() {
var newState = this.getStateFromStores();
@@ -322,7 +304,37 @@ export default class Sidebar extends React.Component {
showBottomUnread
});
}
- createChannelElement(channel, index) {
+
+ handleLeaveDirectChannel(channel) {
+ if (!this.isLeaving.get(channel.id)) {
+ this.isLeaving.set(channel.id, true);
+
+ const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, channel.teammate_id, 'false');
+
+ // bypass AsyncClient since we've already saved the updated preferences
+ Client.savePreferences(
+ [preference],
+ () => {
+ this.isLeaving.set(channel.id, false);
+ },
+ () => {
+ this.isLeaving.set(channel.id, false);
+ }
+ );
+
+ this.setState(this.getStateFromStores());
+ }
+
+ if (channel.id === this.state.activeId) {
+ Utils.switchChannel(ChannelStore.getByName(Constants.DEFAULT_CHANNEL));
+ }
+ }
+
+ sortChannelsByDisplayName(a, b) {
+ return a.display_name.localeCompare(b.display_name);
+ }
+
+ createChannelElement(channel, index, arr, handleClose) {
var members = this.state.members;
var activeId = this.state.activeId;
var channelMember = members[channel.id];
@@ -333,15 +345,16 @@ export default class Sidebar extends React.Component {
linkClass = 'active';
}
+ let rowClass = 'sidebar-channel';
+
var unread = false;
if (channelMember) {
msgCount = channel.total_msg_count - channelMember.msg_count;
unread = (msgCount > 0 && channelMember.notify_props.mark_unread !== 'mention') || channelMember.mention_count > 0;
}
- var titleClass = '';
if (unread) {
- titleClass = 'unread-title';
+ rowClass += ' unread-title';
if (channel.id !== activeId) {
if (!this.firstUnreadChannel) {
@@ -374,9 +387,8 @@ export default class Sidebar extends React.Component {
);
}
- var badgeClass;
if (msgCount > 0) {
- badgeClass = 'has-badge';
+ rowClass += ' has-badge';
}
// set up status icon for direct message channels
@@ -405,8 +417,13 @@ export default class Sidebar extends React.Component {
if (!channel.fake) {
handleClick = function clickHandler(e) {
+ if (e.target.attributes.getNamedItem('data-close')) {
+ handleClose(channel);
+ } else {
+ Utils.switchChannel(channel);
+ }
+
e.preventDefault();
- Utils.switchChannel(channel);
};
} else if (channel.fake && teamURL) {
// It's a direct message channel that doesn't exist yet so let's create it now
@@ -415,23 +432,42 @@ export default class Sidebar extends React.Component {
if (this.state.loadingDMChannel === -1) {
handleClick = function clickHandler(e) {
e.preventDefault();
- this.setState({loadingDMChannel: index});
-
- Client.createDirectChannel(channel, otherUserId,
- function success(data) {
- this.setState({loadingDMChannel: -1});
- AsyncClient.getChannel(data.id);
- Utils.switchChannel(data);
- }.bind(this),
- function error() {
- this.setState({loadingDMChannel: -1});
- window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name;
- }.bind(this)
- );
+
+ if (e.target.attributes.getNamedItem('data-close')) {
+ handleClose(channel);
+ } else {
+ this.setState({loadingDMChannel: index});
+
+ Client.createDirectChannel(channel, otherUserId,
+ (data) => {
+ this.setState({loadingDMChannel: -1});
+ AsyncClient.getChannel(data.id);
+ Utils.switchChannel(data);
+ },
+ () => {
+ this.setState({loadingDMChannel: -1});
+ window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name;
+ }
+ );
+ }
}.bind(this);
}
}
+ let closeButton = null;
+ if (handleClose && !badge) {
+ closeButton = (
+ <span
+ className='close-btn pull-right small'
+ data-close='true'
+ >
+ {'×'}
+ </span>
+ );
+
+ rowClass = ' has-close';
+ }
+
return (
<li
key={channel.name}
@@ -439,13 +475,14 @@ export default class Sidebar extends React.Component {
className={linkClass}
>
<a
- className={'sidebar-channel ' + titleClass + ' ' + badgeClass}
+ className={rowClass}
href={href}
onClick={handleClick}
>
{status}
{channel.display_name}
{badge}
+ {closeButton}
</a>
</li>
);
@@ -464,7 +501,9 @@ export default class Sidebar extends React.Component {
const privateChannels = this.state.channels.filter((channel) => channel.type === 'P');
const privateChannelItems = privateChannels.map(this.createChannelElement);
- const directMessageItems = this.state.showDirectChannels.map(this.createChannelElement);
+ const directMessageItems = this.state.visibleDirectChannels.map((channel, index, arr) => {
+ return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel);
+ });
// update the favicon to show if there are any notifications
var link = document.createElement('link');
@@ -484,17 +523,18 @@ export default class Sidebar extends React.Component {
head.appendChild(link);
var directMessageMore = null;
- if (this.state.hideDirectChannels.length > 0) {
+ if (this.state.hiddenDirectChannels.length > 0) {
directMessageMore = (
- <li>
+ <li key='more'>
<a
+ key={`more${this.state.hiddenDirectChannels.length}`}
href='#'
data-toggle='modal'
className='nav-more'
data-target='#more_direct_channels'
- data-channels={JSON.stringify(this.state.hideDirectChannels)}
+ data-channels={JSON.stringify(this.state.hiddenDirectChannels)}
>
- {'More (' + this.state.hideDirectChannels.length + ')'}
+ {'More (' + this.state.hiddenDirectChannels.length + ')'}
</a>
</li>
);
@@ -538,7 +578,7 @@ export default class Sidebar extends React.Component {
<ul className='nav nav-pills nav-stacked'>
<li>
<h4>
- Channels
+ {'Channels'}
<a
className='add-channel-btn'
href='#'
@@ -557,7 +597,7 @@ export default class Sidebar extends React.Component {
data-target='#more_channels'
data-channeltype='O'
>
- More...
+ {'More...'}
</a>
</li>
</ul>
@@ -565,7 +605,7 @@ export default class Sidebar extends React.Component {
<ul className='nav nav-pills nav-stacked'>
<li>
<h4>
- Private Groups
+ {'Private Groups'}
<a
className='add-channel-btn'
href='#'
@@ -578,7 +618,7 @@ export default class Sidebar extends React.Component {
{privateChannelItems}
</ul>
<ul className='nav nav-pills nav-stacked'>
- <li><h4>Direct Messages</h4></li>
+ <li><h4>{'Direct Messages'}</h4></li>
{directMessageItems}
{directMessageMore}
</ul>
diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx
index ea2bcf9a4..ac101d631 100644
--- a/web/react/components/sidebar_right_menu.jsx
+++ b/web/react/components/sidebar_right_menu.jsx
@@ -75,7 +75,7 @@ export default class SidebarRightMenu extends React.Component {
data-toggle='modal'
data-target='#team_members'
>
- <i className='glyphicon glyphicon-wrench'></i>Manage Team</a>
+ <i className='glyphicon glyphicon-wrench'></i>Manage Members</a>
</li>
);
}
diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx
index 55cccf948..98f862e69 100644
--- a/web/react/components/signup_user_complete.jsx
+++ b/web/react/components/signup_user_complete.jsx
@@ -149,7 +149,7 @@ export default class SignupUserComplete extends React.Component {
// set up the email entry and hide it if an email was provided
var yourEmailIs = '';
if (this.state.user.email) {
- yourEmailIs = <span>Your email address is {this.state.user.email}. You'll use this address to sign in to {global.window.config.SiteName}.</span>;
+ yourEmailIs = <span>Your email address is <strong>{this.state.user.email}</strong>. You'll use this address to sign in to {global.window.config.SiteName}.</span>;
}
var emailContainerStyle = 'margin--extra';
@@ -169,6 +169,7 @@ export default class SignupUserComplete extends React.Component {
placeholder=''
maxLength='128'
autoFocus={true}
+ spellCheck='false'
/>
{emailError}
</div>
@@ -204,6 +205,7 @@ export default class SignupUserComplete extends React.Component {
className='form-control'
placeholder=''
maxLength='128'
+ spellCheck='false'
/>
{nameError}
<span className='help-block'>Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'</span>
@@ -218,6 +220,7 @@ export default class SignupUserComplete extends React.Component {
className='form-control'
placeholder=''
maxLength='128'
+ spellCheck='false'
/>
{passwordError}
</div>
diff --git a/web/react/components/team_import_tab.jsx b/web/react/components/team_import_tab.jsx
index 40f06c382..a80b1a472 100644
--- a/web/react/components/team_import_tab.jsx
+++ b/web/react/components/team_import_tab.jsx
@@ -34,14 +34,14 @@ export default class TeamImportTab extends React.Component {
render() {
var uploadHelpText = (
<div>
- <p>{'Slack does not allow you to export files, images, private groups or direct messages stored in Slack. Therefore, Slack import to Mattermost only supports importing of text messages in your Slack team\'\s public channels.'}</p>
- <p>{'The Slack import to Mattermost is in "Preview". Slack bot posts do not yet import and Slack @mentions are not currently supported.'}</p>
+ <p>{'To import a team from Slack go to Slack > Team Settings > Import/Export Data > Export > Start Export. Slack does not allow you to export files, images, private groups or direct messages stored in Slack. Therefore, Slack import to Mattermost only supports importing of text messages in your Slack team\'\s public channels.'}</p>
+ <p>{'The Slack import to Mattermost is in "Beta". Slack bot posts do not yet import and Slack @mentions are not currently supported.'}</p>
</div>
);
var uploadSection = (
<SettingUpload
- title='Import from Slack'
+ title='Import from Slack (Beta)'
submit={this.doImportSlack}
helpText={uploadHelpText}
fileTypesAccepted='.zip'
diff --git a/web/react/components/team_signup_display_name_page.jsx b/web/react/components/team_signup_display_name_page.jsx
index 65da4bc96..c91ed0811 100644
--- a/web/react/components/team_signup_display_name_page.jsx
+++ b/web/react/components/team_signup_display_name_page.jsx
@@ -66,6 +66,7 @@ export default class TeamSignupDisplayNamePage extends React.Component {
defaultValue={this.props.state.team.display_name}
autoFocus={true}
onFocus={this.handleFocus}
+ spellCheck='false'
/>
</div>
</div>
diff --git a/web/react/components/team_signup_email_item.jsx b/web/react/components/team_signup_email_item.jsx
index 219f14eef..7253e80e9 100644
--- a/web/react/components/team_signup_email_item.jsx
+++ b/web/react/components/team_signup_email_item.jsx
@@ -51,6 +51,7 @@ export default class TeamSignupEmailItem extends React.Component {
placeholder='Email Address'
defaultValue={this.props.email}
maxLength='128'
+ spellCheck='false'
/>
{emailError}
</div>
diff --git a/web/react/components/team_signup_password_page.jsx b/web/react/components/team_signup_password_page.jsx
index b1ccb0dd4..cb9a9f05b 100644
--- a/web/react/components/team_signup_password_page.jsx
+++ b/web/react/components/team_signup_password_page.jsx
@@ -109,6 +109,7 @@ export default class TeamSignupPasswordPage extends React.Component {
className='form-control'
placeholder=''
maxLength='128'
+ spellCheck='false'
/>
<span className='color--light help-block'>Passwords must contain 5 to 50 characters. Your password will be strongest if it contains a mix of symbols, numbers, and upper and lowercase characters.</span>
</div>
diff --git a/web/react/components/team_signup_url_page.jsx b/web/react/components/team_signup_url_page.jsx
index 398a52f7d..3fb0aaa27 100644
--- a/web/react/components/team_signup_url_page.jsx
+++ b/web/react/components/team_signup_url_page.jsx
@@ -111,6 +111,7 @@ export default class TeamSignupUrlPage extends React.Component {
defaultValue={this.props.state.team.name}
autoFocus={true}
onFocus={this.handleFocus}
+ spellCheck='false'
/>
</div>
</div>
diff --git a/web/react/components/team_signup_username_page.jsx b/web/react/components/team_signup_username_page.jsx
index 198c4b200..82dabad3d 100644
--- a/web/react/components/team_signup_username_page.jsx
+++ b/web/react/components/team_signup_username_page.jsx
@@ -68,6 +68,7 @@ export default class TeamSignupUsernamePage extends React.Component {
placeholder=''
defaultValue={this.props.state.user.username}
maxLength='128'
+ spellCheck='false'
/>
<span className='color--light help-block'>Usernames must begin with a letter and contain 3 to 15 characters made up of lowercase letters, numbers, and the symbols '.', '-' and '_'</span>
</div>
diff --git a/web/react/components/team_signup_welcome_page.jsx b/web/react/components/team_signup_welcome_page.jsx
index 78a41eed5..2d7ef081f 100644
--- a/web/react/components/team_signup_welcome_page.jsx
+++ b/web/react/components/team_signup_welcome_page.jsx
@@ -145,6 +145,7 @@ export default class TeamSignupWelcomePage extends React.Component {
className='form-control'
placeholder='Email Address'
maxLength='128'
+ spellCheck='false'
/>
</div>
</div>
diff --git a/web/react/components/team_signup_with_email.jsx b/web/react/components/team_signup_with_email.jsx
index 9376a4564..ba32a9f97 100644
--- a/web/react/components/team_signup_with_email.jsx
+++ b/web/react/components/team_signup_with_email.jsx
@@ -63,6 +63,7 @@ export default class EmailSignUpPage extends React.Component {
className='form-control'
placeholder='Email Address'
maxLength='128'
+ spellCheck='false'
/>
</div>
<div className='form-group'>
diff --git a/web/react/components/team_signup_with_sso.jsx b/web/react/components/team_signup_with_sso.jsx
index 011bfebff..6ccf762c1 100644
--- a/web/react/components/team_signup_with_sso.jsx
+++ b/web/react/components/team_signup_with_sso.jsx
@@ -104,6 +104,7 @@ export default class SSOSignUpPage extends React.Component {
placeholder='Enter name of new team'
maxLength='128'
onChange={this.nameChange}
+ spellCheck='false'
/>
{nameError}
</div>
diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx
index 0563c294a..741dbcd5d 100644
--- a/web/react/components/textbox.jsx
+++ b/web/react/components/textbox.jsx
@@ -246,9 +246,11 @@ export default class Textbox extends React.Component {
if (e.scrollHeight - mod < 167) {
$(e).css({height: 'auto', 'overflow-y': 'hidden'}).height(e.scrollHeight - mod);
$(w).css({height: 'auto'}).height(e.scrollHeight + 2);
+ $(w).closest('.post-body__cell').removeClass('scroll');
} else {
$(e).css({height: 'auto', 'overflow-y': 'scroll'}).height(167);
$(w).css({height: 'auto'}).height(167);
+ $(w).closest('.post-body__cell').addClass('scroll');
}
if (prevHeight !== $(e).height() && this.props.onHeightChange) {
diff --git a/web/react/components/user_settings/manage_incoming_hooks.jsx b/web/react/components/user_settings/manage_incoming_hooks.jsx
index 90b4039c7..8817e0d39 100644
--- a/web/react/components/user_settings/manage_incoming_hooks.jsx
+++ b/web/react/components/user_settings/manage_incoming_hooks.jsx
@@ -148,7 +148,7 @@ export default class ManageIncomingHooks extends React.Component {
return (
<div key='addIncomingHook'>
- {'Create webhook URLs for channels and private groups. These URLs can be used by outside applications to create posts in any channels or private groups you have access to. The specified channel will be used as the default.'}
+ {'For developers building integrations this page lets you create webhook URLs for channels and private groups. Please see http://mattermost.org/webhooks to learn about creating webhooks, view samples, and to let the community know about integrations you have built. The URLs created below can be used by outside applications to create posts in any channels or private groups you have access to. The specified channel will be used as the default.'}
<br/>
<label className='control-label'>{'Add a new incoming webhook'}</label>
<div className='padding-top'>
diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx
index 6d64e83b6..a16440d55 100644
--- a/web/react/components/user_settings/user_settings_appearance.jsx
+++ b/web/react/components/user_settings/user_settings_appearance.jsx
@@ -178,7 +178,7 @@ export default class UserSettingsAppearance extends React.Component {
href='#'
onClick={this.submitTheme}
>
- {'Submit'}
+ {'Save'}
</a>
<a
className='btn btn-sm theme'
diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx
index c23c61948..66d83725c 100644
--- a/web/react/components/user_settings/user_settings_general.jsx
+++ b/web/react/components/user_settings/user_settings_general.jsx
@@ -368,8 +368,7 @@ export default class UserSettingsGeneralTab extends React.Component {
const extraInfo = (
<span>
- {'Use Nickname for a name you might be called that is different from your first name and user name.'}
- {'This is most often used when two or more people have similar sounding names and usernames.'}
+ {'Use Nickname for a name you might be called that is different from your first name and username. This is most often used when two or more people have similar sounding names and usernames.'}
</span>
);
diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx
index ea3f72f27..3be062ad3 100644
--- a/web/react/components/user_settings/user_settings_integrations.jsx
+++ b/web/react/components/user_settings/user_settings_integrations.jsx
@@ -38,6 +38,7 @@ export default class UserSettingsIntegrationsTab extends React.Component {
incomingHooksSection = (
<SettingItemMax
title='Incoming Webhooks'
+ width = 'full'
inputs={inputs}
updateSection={function clearSection(e) {
this.updateSection('');
@@ -49,7 +50,8 @@ export default class UserSettingsIntegrationsTab extends React.Component {
incomingHooksSection = (
<SettingItemMin
title='Incoming Webhooks'
- describe='Manage your incoming webhooks'
+ width = 'full'
+ describe='Manage your incoming webhooks (Developer feature)'
updateSection={function updateNameSection() {
this.updateSection('incoming-hooks');
}.bind(this)}
diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx
index 8ef68dd0a..c5f0abc12 100644
--- a/web/react/components/view_image.jsx
+++ b/web/react/components/view_image.jsx
@@ -195,6 +195,7 @@ export default class ViewImageModal extends React.Component {
target='_blank'
>
<img
+ style={{maxHeight: this.state.imgHeight}}
ref='image'
src={this.getPreviewImagePath(filename)}
/>
@@ -210,6 +211,7 @@ export default class ViewImageModal extends React.Component {
content = (
<video
+ style={{maxHeight: this.state.imgHeight}}
ref='video'
data-setup='{}'
controls='controls'
@@ -334,7 +336,6 @@ export default class ViewImageModal extends React.Component {
>
<div
className={'image-wrapper ' + bgClass}
- style={{maxHeight: this.state.imgHeight}}
onMouseEnter={this.onMouseEnterImage}
onMouseLeave={this.onMouseLeaveImage}
onClick={(e) => e.stopPropagation()}
diff --git a/web/react/stores/preference_store.jsx b/web/react/stores/preference_store.jsx
new file mode 100644
index 000000000..d71efa10f
--- /dev/null
+++ b/web/react/stores/preference_store.jsx
@@ -0,0 +1,122 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const ActionTypes = require('../utils/constants.jsx').ActionTypes;
+const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
+const BrowserStore = require('./browser_store.jsx');
+const EventEmitter = require('events').EventEmitter;
+const UserStore = require('../stores/user_store.jsx');
+
+const CHANGE_EVENT = 'change';
+
+function getPreferenceKey(category, name) {
+ return `${category}-${name}`;
+}
+
+function getPreferenceKeyForModel(preference) {
+ return `${preference.category}-${preference.name}`;
+}
+
+class PreferenceStoreClass extends EventEmitter {
+ constructor() {
+ super();
+
+ this.getAllPreferences = this.getAllPreferences.bind(this);
+ this.getPreference = this.getPreference.bind(this);
+ this.getPreferences = this.getPreferences.bind(this);
+ this.getPreferencesWhere = this.getPreferencesWhere.bind(this);
+ this.setAllPreferences = this.setAllPreferences.bind(this);
+ this.setPreference = this.setPreference.bind(this);
+
+ this.emitChange = this.emitChange.bind(this);
+ this.addChangeListener = this.addChangeListener.bind(this);
+ this.removeChangeListener = this.removeChangeListener.bind(this);
+
+ this.handleEventPayload = this.handleEventPayload.bind(this);
+ this.dispatchToken = AppDispatcher.register(this.handleEventPayload);
+ }
+
+ getAllPreferences() {
+ return new Map(BrowserStore.getItem('preferences', []));
+ }
+
+ getPreference(category, name, defaultValue = '') {
+ return this.getAllPreferences().get(getPreferenceKey(category, name)) || defaultValue;
+ }
+
+ getPreferences(category) {
+ return this.getPreferencesWhere((preference) => (preference.category === category));
+ }
+
+ getPreferencesWhere(pred) {
+ const all = this.getAllPreferences();
+ const preferences = [];
+
+ for (const [, preference] of all) {
+ if (pred(preference)) {
+ preferences.push(preference);
+ }
+ }
+
+ return preferences;
+ }
+
+ setAllPreferences(preferences) {
+ // note that we store the preferences as an array of key-value pairs so that we can deserialize
+ // it as a proper Map instead of an object
+ BrowserStore.setItem('preferences', [...preferences]);
+ }
+
+ setPreference(category, name, value) {
+ const preferences = this.getAllPreferences();
+
+ const key = getPreferenceKey(category, name);
+ let preference = preferences.get(key);
+
+ if (!preference) {
+ preference = {
+ user_id: UserStore.getCurrentId(),
+ category,
+ name
+ };
+ }
+ preference.value = value;
+
+ preferences.set(key, preference);
+
+ this.setAllPreferences(preferences);
+
+ return preference;
+ }
+
+ emitChange(preferences) {
+ this.emit(CHANGE_EVENT, preferences);
+ }
+
+ addChangeListener(callback) {
+ this.on(CHANGE_EVENT, callback);
+ }
+
+ removeChangeListener(callback) {
+ this.removeListener(CHANGE_EVENT, callback);
+ }
+
+ handleEventPayload(payload) {
+ const action = payload.action;
+
+ switch (action.type) {
+ case ActionTypes.RECIEVED_PREFERENCES:
+ const preferences = this.getAllPreferences();
+
+ for (const preference of action.preferences) {
+ preferences.set(getPreferenceKeyForModel(preference), preference);
+ }
+
+ this.setAllPreferences(preferences);
+ this.emitChange(preferences);
+ }
+ }
+}
+
+const PreferenceStore = new PreferenceStoreClass();
+export default PreferenceStore;
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index a903f055b..1bf8a6fee 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -637,3 +637,55 @@ export function getMyTeam() {
}
);
}
+
+export function getDirectChannelPreferences() {
+ if (isCallInProgress('getDirectChannelPreferences')) {
+ return;
+ }
+
+ callTracker.getDirectChannelPreferences = utils.getTimestamp();
+ client.getPreferenceCategory(
+ Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
+ (data, textStatus, xhr) => {
+ callTracker.getDirectChannelPreferences = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_PREFERENCES,
+ preferences: data
+ });
+ },
+ (err) => {
+ callTracker.getDirectChannelPreferences = 0;
+ dispatchError(err, 'getDirectChannelPreferences');
+ }
+ );
+}
+
+export function savePreferences(preferences, success, error) {
+ client.savePreferences(
+ preferences,
+ (data, textStatus, xhr) => {
+ if (xhr.status !== 304) {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_PREFERENCES,
+ preferences
+ });
+ }
+
+ if (success) {
+ success(data);
+ }
+ },
+ (err) => {
+ dispatchError(err, 'savePreferences');
+
+ if (error) {
+ error();
+ }
+ }
+ );
+}
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index 6dccfcdeb..76a402855 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -1141,3 +1141,31 @@ export function listIncomingHooks(success, error) {
}
});
}
+
+export function getPreferenceCategory(category, success, error) {
+ $.ajax({
+ url: `/api/v1/preferences/${category}`,
+ dataType: 'json',
+ type: 'GET',
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('getPreferenceCategory', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function savePreferences(preferences, success, error) {
+ $.ajax({
+ url: '/api/v1/preferences/save',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(preferences),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('savePreferences', xhr, status, err);
+ error(e);
+ }
+ });
+}
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index affc49196..cee2ec114 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -28,6 +28,7 @@ module.exports = {
RECIEVED_AUDITS: null,
RECIEVED_TEAMS: null,
RECIEVED_STATUSES: null,
+ RECIEVED_PREFERENCES: null,
RECIEVED_MSG: null,
@@ -201,7 +202,7 @@ module.exports = {
centerChannelBg: '#1F1F1F',
centerChannelColor: '#DDDDDD',
newMessageSeparator: '#CC992D',
- linkColor: '#0177e7',
+ linkColor: '#0D93FF',
buttonBg: '#0177e7',
buttonColor: '#FFFFFF',
mentionHighlightBg: '#784098',
@@ -285,5 +286,8 @@ module.exports = {
id: 'mentionHighlightLink',
uiName: 'Mention Highlight Link'
}
- ]
+ ],
+ Preferences: {
+ CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show'
+ }
};
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index f9166063e..91502352e 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -423,7 +423,10 @@ export function applyTheme(theme) {
if (theme.sidebarTextActiveColor) {
changeCss('.sidebar--left .nav-pills__container li.active a, .sidebar--left .nav-pills__container li.active a:hover, .sidebar--left .nav-pills__container li.active a:focus, .settings-modal .nav-pills>li.active a, .settings-modal .nav-pills>li.active a:hover, .settings-modal .nav-pills>li.active a:active', 'color:' + theme.sidebarTextActiveColor, 2);
- changeCss('.sidebar--left .nav-pills__container li.active a .status .online--icon', 'fill:' + theme.sidebarTextActiveColor, 2);
+ }
+
+ if (theme.sidebarTextActiveBg === theme.onlineIndicator) {
+ changeCss('.sidebar--left .nav-pills__container li.active a .status .online--icon', 'fill:' + theme.sidebarTextActiveColor, 1);
}
if (theme.sidebarHeaderBg) {
@@ -497,7 +500,7 @@ export function applyTheme(theme) {
changeCss('.date-separator .separator__hr, .modal-footer, .modal .custom-textarea, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.modal .custom-textarea:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
changeCss('.channel-intro, .settings-modal .settings-table .settings-content .divider-dark, hr, .settings-modal .settings-table .settings-links', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
- changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, pre', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, .post.post--comment.other--root .post-comment, .post.same--root .post-body, .modal .more-channel-table tbody>tr td, .member-div:first-child, .member-div, .access-history__table .access__report, .activity-log__table', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.1), 2);
changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
@@ -505,6 +508,7 @@ export function applyTheme(theme) {
changeCss('.post:hover, .modal .more-channel-table tbody>tr:hover td, .sidebar--right .sidebar--right__header, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.date-separator.hovered--before:after, .date-separator.hovered--after:before, .new-separator.hovered--after:before, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.command-name:hover, .mentions-name:hover, .mentions-focus, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
+ changeCss('code', 'background:' + changeOpacity(theme.centerChannelColor, 0.1), 1);
changeCss('.post.current--user:hover .post-body ', 'background: none;', 1);
changeCss('.sidebar--right', 'color:' + theme.centerChannelColor, 2);
}
diff --git a/web/sass-files/sass/partials/_admin-console.scss b/web/sass-files/sass/partials/_admin-console.scss
index 09907da6d..4b4fc1664 100644
--- a/web/sass-files/sass/partials/_admin-console.scss
+++ b/web/sass-files/sass/partials/_admin-console.scss
@@ -117,6 +117,9 @@
.form-group {
margin-bottom: 25px;
}
+ ul, ol {
+ padding-left: 23px;
+ }
.help-text {
margin: 10px 0 0 15px;
color: #777;
diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss
index 18462d92a..470db16dc 100644
--- a/web/sass-files/sass/partials/_base.scss
+++ b/web/sass-files/sass/partials/_base.scss
@@ -34,6 +34,11 @@ body {
}
}
+img {
+ max-width: 100%;
+ height: auto;
+}
+
.input-group-addon {
background: transparent;
}
diff --git a/web/sass-files/sass/partials/_content.scss b/web/sass-files/sass/partials/_content.scss
index c8c205047..64d253283 100644
--- a/web/sass-files/sass/partials/_content.scss
+++ b/web/sass-files/sass/partials/_content.scss
@@ -28,6 +28,7 @@
bottom: 0;
left: 0;
width: 100%;
+ z-index: 6;
}
.post-list {
.new-messages-hr {
diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss
index 8e353aff9..feaa5acfb 100644
--- a/web/sass-files/sass/partials/_headers.scss
+++ b/web/sass-files/sass/partials/_headers.scss
@@ -147,7 +147,8 @@
}
.header__info {
color: #fff;
- padding-left: 4px;
+ @include clearfix;
+ padding-left: 2px;
z-index: 1;
position: relative;
}
@@ -155,7 +156,7 @@
display: block;
font-weight: 600;
font-size: 16px;
- max-width: 80%;
+ max-width: 85%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
diff --git a/web/sass-files/sass/partials/_markdown.scss b/web/sass-files/sass/partials/_markdown.scss
index 122586354..1aa942ad0 100644
--- a/web/sass-files/sass/partials/_markdown.scss
+++ b/web/sass-files/sass/partials/_markdown.scss
@@ -53,15 +53,10 @@ blockquote {
}
pre {
border: none;
- background-color: #f7f7f7;
margin: 5px 0;
- .current--user & {
- background: #fff;
- }
- code {
- color: #c7254e;
- }
+ color: inherit;
}
code {
background: #fff;
+ color: inherit;
}
diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss
index 2722333a4..90ea8ce2c 100644
--- a/web/sass-files/sass/partials/_modal.scss
+++ b/web/sass-files/sass/partials/_modal.scss
@@ -1,3 +1,6 @@
+#channel_members_modal .modal-body {
+ min-height: 110px;
+}
.modal-body {
padding: 20px 15px;
}
diff --git a/web/sass-files/sass/partials/_navbar.scss b/web/sass-files/sass/partials/_navbar.scss
index 2e78a8728..a8c3c0da2 100644
--- a/web/sass-files/sass/partials/_navbar.scss
+++ b/web/sass-files/sass/partials/_navbar.scss
@@ -95,6 +95,12 @@
}
}
+.close-btn {
+ position: absolute;
+ right: 10px;
+ top: 5px;
+}
+
.badge-notify {
background:red;
position: absolute;
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index ccd7fd425..b457d0dd2 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -147,12 +147,12 @@ body.ios {
&::-webkit-scrollbar {
width: 0px !important;
}
- &.inactive {
- display: none;
- }
- &.active {
- display: inline;
- }
+ &.inactive {
+ display: none;
+ }
+ &.active {
+ display: inline;
+ }
}
.post-list__table {
display: table;
@@ -195,6 +195,14 @@ body.ios {
.post-body__cell {
vertical-align: top;
position: relative;
+ &.scroll {
+ .btn-file {
+ right: 15px;
+ }
+ .custom-textarea {
+ padding-right: 43px;
+ }
+ }
}
.send-button {
display: none;
@@ -399,6 +407,8 @@ body.ios {
display: none;
}
.post-body {
+ position: relative;
+ z-index: 5;
max-width: 100%;
width: 600px;
float: left;
@@ -423,10 +433,13 @@ body.ios {
}
}
.post-header {
+ position: relative;
list-style-type: none;
margin: 0 0 1px;
padding-left: 0px;
&.post-header-post {
+ position: relative;
+ z-index: 5;
width: 200px;
text-align: right;
float: left;
@@ -511,9 +524,9 @@ body.ios {
}
.bot-indicator {
- background-color: lightgrey;
- border-radius:2px;
- padding-left:2px;
- padding-right:2px;
- font-family:"Courier New"
+ background-color: lightgrey;
+ border-radius:2px;
+ padding-left:2px;
+ padding-right:2px;
+ font-family:"Courier New"
}
diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss
index 82ec1811a..447f47d87 100644
--- a/web/sass-files/sass/partials/_responsive.scss
+++ b/web/sass-files/sass/partials/_responsive.scss
@@ -73,6 +73,10 @@
display: inline;
.post-info {
display: inline;
+ .tooltip {
+ margin-top: -25px;
+ margin-left: 40px;
+ }
.post-profile-time {
margin: 0;
}
@@ -153,6 +157,10 @@
display: inline;
.post-info {
display: inline;
+ .tooltip {
+ margin-top: -25px;
+ margin-left: 40px;
+ }
.post-profile-time {
width: auto;
margin: 0;
@@ -327,6 +335,7 @@
}
.modal-title {
float: none;
+ max-width: 90%;
}
.btn {
&.btn-primary {
@@ -421,6 +430,9 @@
.post-body__cell {
display: table-cell;
padding-left: 45px;
+ .sidebar--right & {
+ padding-left: 0;
+ }
}
.app__content & {
.btn-file {
@@ -667,7 +679,7 @@
.modal-image {
.image-wrapper {
font-size: 12px;
- max-width: 280px;
+ min-width: 280px;
.modal-close {
@include opacity(1);
}
diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss
index 8debb0b4e..0c2f25eab 100644
--- a/web/sass-files/sass/partials/_settings.scss
+++ b/web/sass-files/sass/partials/_settings.scss
@@ -268,7 +268,6 @@
position:absolute;
right:15px;
top:13px;
- color:#414142;
}
}
diff --git a/web/sass-files/sass/partials/_sidebar--left.scss b/web/sass-files/sass/partials/_sidebar--left.scss
index 73d702fef..7cb530d2c 100644
--- a/web/sass-files/sass/partials/_sidebar--left.scss
+++ b/web/sass-files/sass/partials/_sidebar--left.scss
@@ -98,6 +98,9 @@
&.has-badge {
padding-right: 30px;
}
+ &.has-close {
+ padding-right: 30px;
+ }
&.nav-more {
text-decoration: underline;
}
diff --git a/web/sass-files/sass/partials/_sidebar--right.scss b/web/sass-files/sass/partials/_sidebar--right.scss
index b37dbf421..c954b03d8 100644
--- a/web/sass-files/sass/partials/_sidebar--right.scss
+++ b/web/sass-files/sass/partials/_sidebar--right.scss
@@ -26,6 +26,7 @@
.post-header {
.post-header-col {
&.post-header__reply {
+ min-width: 30px;
text-align: right;
float: right;
}
diff --git a/web/templates/channel.html b/web/templates/channel.html
index 2af94e415..13fd80d75 100644
--- a/web/templates/channel.html
+++ b/web/templates/channel.html
@@ -54,7 +54,7 @@
<script>
window.setup_channel_page({{ .Props }});
$('body').tooltip( {selector: '[data-toggle=tooltip]'} );
- $('.modal-body').css('max-height', $(window).height() * 0.7);
+ $('.modal-body').css('max-height', $(window).height() - 150);
$('.modal-body').perfectScrollbar();
</script>
</body>
diff --git a/web/templates/head.html b/web/templates/head.html
index 8039f48a1..e4f1b56b3 100644
--- a/web/templates/head.html
+++ b/web/templates/head.html
@@ -37,7 +37,7 @@
<script src="/static/js/bootstrap-3.3.5.js"></script>
<script src="/static/js/bootstrap-colorpicker.min.js"></script>
<script src="/static/js/react-bootstrap-0.25.1.js"></script>
- <script src="/static/js/perfect-scrollbar-0.6.5.jquery.js"></script>
+ <script src="/static/js/perfect-scrollbar-0.6.5.jquery.min.js"></script>
<script src="/static/js/jquery-dragster/jquery.dragster.js"></script>
<style id="antiClickjack">body{display:none !important;}</style>