summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJack <jackdeng@gmail.com>2015-09-28 08:14:57 -0700
committerJack <jackdeng@gmail.com>2015-09-28 08:14:57 -0700
commit503501ab3b616fb9fa57b046df850cfc2db734f8 (patch)
tree43e529250bab982c73816f1ffdf673e8e4b98c13
parentaa6da728baf2f5642293d8b56ebc1ff4e8682bce (diff)
parenta055fd10b83b28bf97877e4e28a2275f74a71a4a (diff)
downloadchat-503501ab3b616fb9fa57b046df850cfc2db734f8.tar.gz
chat-503501ab3b616fb9fa57b046df850cfc2db734f8.tar.bz2
chat-503501ab3b616fb9fa57b046df850cfc2db734f8.zip
rebase
-rw-r--r--CHANGELOG.md25
-rw-r--r--Godeps/Godeps.json4
-rw-r--r--Godeps/_workspace/src/github.com/mssola/user_agent/.travis.yml1
-rw-r--r--Godeps/_workspace/src/github.com/mssola/user_agent/all_test.go8
-rw-r--r--Godeps/_workspace/src/github.com/mssola/user_agent/browser.go18
-rw-r--r--NOTICE.txt73
-rw-r--r--api/context.go19
-rw-r--r--api/export.go2
-rw-r--r--api/file.go14
-rw-r--r--api/import.go4
-rw-r--r--api/team.go95
-rw-r--r--api/team_test.go33
-rw-r--r--api/templates/error.html2
-rw-r--r--api/templates/signup_team_body.html3
-rw-r--r--api/templates/welcome_body.html12
-rw-r--r--api/templates/welcome_subject.html2
-rw-r--r--api/user.go106
-rw-r--r--api/user_test.go21
-rw-r--r--config/config.json1
-rw-r--r--doc/README.md4
-rw-r--r--doc/help/enduser/markdown.md152
-rw-r--r--doc/install/single-container-install.md8
-rw-r--r--docker/dev/config_docker.json1
-rw-r--r--docker/local/config_docker.json1
-rw-r--r--mattermost.go4
-rw-r--r--model/client.go30
-rw-r--r--model/config.go1
-rw-r--r--model/team.go20
-rw-r--r--model/user.go1
-rw-r--r--store/sql_session_store.go18
-rw-r--r--store/sql_session_store_test.go23
-rw-r--r--store/sql_team_store.go4
-rw-r--r--store/sql_user_store.go19
-rw-r--r--store/sql_user_store_test.go18
-rw-r--r--store/store.go4
-rw-r--r--web/react/components/activity_log_modal.jsx7
-rw-r--r--web/react/components/admin_console/admin_controller.jsx84
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx189
-rw-r--r--web/react/components/admin_console/email_settings.jsx18
-rw-r--r--web/react/components/admin_console/gitlab_settings.jsx16
-rw-r--r--web/react/components/admin_console/image_settings.jsx2
-rw-r--r--web/react/components/admin_console/log_settings.jsx12
-rw-r--r--web/react/components/admin_console/privacy_settings.jsx4
-rw-r--r--web/react/components/admin_console/rate_settings.jsx12
-rw-r--r--web/react/components/admin_console/reset_password_modal.jsx132
-rw-r--r--web/react/components/admin_console/select_team_modal.jsx193
-rw-r--r--web/react/components/admin_console/service_settings.jsx76
-rw-r--r--web/react/components/admin_console/sql_settings.jsx2
-rw-r--r--web/react/components/admin_console/team_settings.jsx28
-rw-r--r--web/react/components/admin_console/team_users.jsx178
-rw-r--r--web/react/components/admin_console/user_item.jsx266
-rw-r--r--web/react/components/channel_loader.jsx9
-rw-r--r--web/react/components/channel_notifications.jsx16
-rw-r--r--web/react/components/create_comment.jsx13
-rw-r--r--web/react/components/create_post.jsx55
-rw-r--r--web/react/components/delete_channel_modal.jsx3
-rw-r--r--web/react/components/error_bar.jsx85
-rw-r--r--web/react/components/file_attachment.jsx10
-rw-r--r--web/react/components/file_attachment_list.jsx16
-rw-r--r--web/react/components/file_upload.jsx3
-rw-r--r--web/react/components/navbar.jsx2
-rw-r--r--web/react/components/post_body.jsx1
-rw-r--r--web/react/components/post_info.jsx16
-rw-r--r--web/react/components/post_list.jsx16
-rw-r--r--web/react/components/rhs_comment.jsx6
-rw-r--r--web/react/components/rhs_root_post.jsx6
-rw-r--r--web/react/components/settings_sidebar.jsx3
-rw-r--r--web/react/components/signup_user_complete.jsx2
-rw-r--r--web/react/components/team_feature_tab.jsx190
-rw-r--r--web/react/components/team_import_tab.jsx2
-rw-r--r--web/react/components/team_settings.jsx14
-rw-r--r--web/react/components/team_settings_modal.jsx13
-rw-r--r--web/react/components/team_signup_password_page.jsx2
-rw-r--r--web/react/components/team_signup_with_sso.jsx2
-rw-r--r--web/react/components/textbox.jsx53
-rw-r--r--web/react/components/user_settings/custom_theme_chooser.jsx7
-rw-r--r--web/react/components/user_settings/manage_incoming_hooks.jsx65
-rw-r--r--web/react/components/user_settings/premade_theme_chooser.jsx5
-rw-r--r--web/react/components/user_settings/user_settings_appearance.jsx2
-rw-r--r--web/react/components/user_settings/user_settings_notifications.jsx6
-rw-r--r--web/react/components/view_image.jsx250
-rw-r--r--web/react/components/view_image_popover_bar.jsx66
-rw-r--r--web/react/pages/channel.jsx6
-rw-r--r--web/react/stores/admin_store.jsx40
-rw-r--r--web/react/stores/error_store.jsx2
-rw-r--r--web/react/stores/socket_store.jsx42
-rw-r--r--web/react/utils/async_client.jsx26
-rw-r--r--web/react/utils/client.jsx48
-rw-r--r--web/react/utils/constants.jsx100
-rw-r--r--web/react/utils/emoticons.jsx7
-rw-r--r--web/react/utils/markdown.jsx16
-rw-r--r--web/react/utils/utils.jsx30
-rw-r--r--web/sass-files/sass/partials/_admin-console.scss17
-rw-r--r--web/sass-files/sass/partials/_base.scss14
-rw-r--r--web/sass-files/sass/partials/_files.scss9
-rw-r--r--web/sass-files/sass/partials/_loading.scss2
-rw-r--r--web/sass-files/sass/partials/_markdown.scss43
-rw-r--r--web/sass-files/sass/partials/_mentions.scss9
-rw-r--r--web/sass-files/sass/partials/_modal.scss42
-rw-r--r--web/sass-files/sass/partials/_post.scss14
-rw-r--r--web/sass-files/sass/partials/_settings.scss15
-rw-r--r--web/sass-files/sass/partials/_videos.scss10
-rw-r--r--web/static/images/themes/mattermost dark.png (renamed from web/static/images/themes/dark.png)bin75371 -> 75371 bytes
-rw-r--r--web/static/images/themes/organization.pngbin0 -> 86044 bytes
-rw-r--r--web/static/images/themes/windows dark.pngbin0 -> 82784 bytes
-rw-r--r--web/web.go3
106 files changed, 2363 insertions, 1041 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2abbb6460..2ee47ec35 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,6 @@
# Mattermost Changelog
-## UNDER DEVELOPMENT - Release v0.8.0 (Beta2)
+## UNDER DEVELOPMENT - Release v1.0.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.
@@ -15,11 +15,23 @@ The "UNDER DEVELOPMENT" section of the Mattermost changelog appears in the produ
Messaging, Comments and Notifications
-- (Preview) Added support for emoji codes rendering to image files
+- Support for emoji codes rendering to image files
+- Full markdown support in messages, comments, and channel description
+- Added ability to play video and audio files
-Admin Console
+System Console
-- (Preview) Ability to view server logs and change config settings
+- UI to change config.json settings
+- Ability to view log files from console
+
+User Interface
+
+- Ability to set custom theme colors
+- Replaced single color themes with pre-set themes
+- Added ability to import themes from Slack
+
+Integrations
+- (Preview) Initial support for incoming webhooks
### Improvements
@@ -30,6 +42,7 @@ Documentation
- Added Code Contribution Guidelines
- Added new hardware sizing recommendations
- Consolidated licensing information into LICENSE.txt and NOTICE.txt
+- Added markdown documentation
Performance
@@ -41,11 +54,11 @@ Code Quality
UI
-- Added version, build number, build date and build hash under Account Settings -> Security (to be moved to "About" dialog later)
+- Added version, build number, build date and build hash under Account Settings -> Security
### Bug Fixes
-- Fixed performance issue with slow typing on iOS
+- Numerous performance improvements
- Fixed issue so that SSO option automatically set EmailVerified=true (it was false previously)
### Contributors
diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json
index 671c37f91..7c80558c4 100644
--- a/Godeps/Godeps.json
+++ b/Godeps/Godeps.json
@@ -84,8 +84,8 @@
},
{
"ImportPath": "github.com/mssola/user_agent",
- "Comment": "v0.4.1-2-g35c7f18",
- "Rev": "35c7f18f5261cc18c698a461053c119aebaf8542"
+ "Comment": "v0.4.1-4-ga163d6a",
+ "Rev": "a163d6a569f1cd264d2f8b2bf3c5d04ace5995eb"
},
{
"ImportPath": "github.com/rwcarlsen/goexif/exif",
diff --git a/Godeps/_workspace/src/github.com/mssola/user_agent/.travis.yml b/Godeps/_workspace/src/github.com/mssola/user_agent/.travis.yml
index 922091263..33c596acb 100644
--- a/Godeps/_workspace/src/github.com/mssola/user_agent/.travis.yml
+++ b/Godeps/_workspace/src/github.com/mssola/user_agent/.travis.yml
@@ -5,6 +5,7 @@ go:
- 1.2
- 1.3
- 1.4
+ - 1.5
- tip
matrix:
allow_failures:
diff --git a/Godeps/_workspace/src/github.com/mssola/user_agent/all_test.go b/Godeps/_workspace/src/github.com/mssola/user_agent/all_test.go
index 6dca19d5c..34ccbb864 100644
--- a/Godeps/_workspace/src/github.com/mssola/user_agent/all_test.go
+++ b/Godeps/_workspace/src/github.com/mssola/user_agent/all_test.go
@@ -40,6 +40,10 @@ var uastrings = []struct {
{"IE11b32Win7b64MDDRJS", "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; MDDRJS; rv:11.0) like Gecko"},
{"IE11Compatibility", "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.3; Trident/7.0)"},
+ // Microsoft Edge
+ {"EdgeDesktop", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240"},
+ {"EdgeMobile", "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; DEVICE INFO) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Mobile Safari/537.36 Edge/12.10240"},
+
// Gecko
{"FirefoxMac", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0b8) Gecko/20100101 Firefox/4.0b8"},
{"FirefoxMacLoc", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13"},
@@ -129,6 +133,10 @@ var expected = []string{
"Mozilla:5.0 Platform:Windows OS:Windows 7 Browser:Internet Explorer-11.0 Engine:Trident Bot:false Mobile:false",
"Mozilla:4.0 Platform:Windows OS:Windows 8.1 Browser:Internet Explorer-7.0 Engine:Trident Bot:false Mobile:false",
+ // Microsoft Edge
+ "Mozilla:5.0 Platform:Windows OS:Windows NT 10.0 Browser:Edge-12.10240 Engine:EdgeHTML Bot:false Mobile:false",
+ "Mozilla:5.0 Platform:Windows OS:Windows Phone 10.0 Browser:Edge-12.10240 Engine:EdgeHTML Bot:false Mobile:true",
+
// Gecko
"Mozilla:5.0 Platform:Macintosh OS:Intel Mac OS X 10.6 Browser:Firefox-4.0b8 Engine:Gecko-20100101 Bot:false Mobile:false",
"Mozilla:5.0 Platform:Macintosh OS:Intel Mac OS X 10.6 Localization:en-US Browser:Firefox-3.6.13 Engine:Gecko-20101203 Bot:false Mobile:false",
diff --git a/Godeps/_workspace/src/github.com/mssola/user_agent/browser.go b/Godeps/_workspace/src/github.com/mssola/user_agent/browser.go
index 9fb27e5f3..c5612db7b 100644
--- a/Godeps/_workspace/src/github.com/mssola/user_agent/browser.go
+++ b/Godeps/_workspace/src/github.com/mssola/user_agent/browser.go
@@ -48,13 +48,21 @@ func (p *UserAgent) detectBrowser(sections []section) {
if slen > 2 {
p.browser.Version = sections[2].version
if engine.name == "AppleWebKit" {
- if sections[slen-1].name == "OPR" {
+ switch sections[slen-1].name {
+ case "Edge":
+ p.browser.Name = "Edge"
+ p.browser.Version = sections[slen-1].version
+ p.browser.Engine = "EdgeHTML"
+ p.browser.EngineVersion = ""
+ case "OPR":
p.browser.Name = "Opera"
p.browser.Version = sections[slen-1].version
- } else if sections[2].name == "Chrome" {
- p.browser.Name = "Chrome"
- } else {
- p.browser.Name = "Safari"
+ default:
+ if sections[2].name == "Chrome" {
+ p.browser.Name = "Chrome"
+ } else {
+ p.browser.Name = "Safari"
+ }
}
} else if engine.name == "Gecko" {
name := sections[2].name
diff --git a/NOTICE.txt b/NOTICE.txt
index f31472477..b7a7fbc1d 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -724,6 +724,77 @@ Legal Terms
Our home page can be found at
http://www.freetype.org
+
+--- end of FTL.TXT ---
+
+---
+This product contains a modified portion of 'gemoji', a collection of emoji images and names by Apple Inc. and other contributors.
---- end of FTL.TXT ---
+* HOMEPAGE:
+ * https://github.com/github/gemoji/blob/master/LICENSE
+
+* LICENSE:
+
+octocat, squirrel, shipit
+Copyright (c) 2013 GitHub Inc. All rights reserved.
+
+bowtie, neckbeard, fu
+Copyright (c) 2013 37signals, LLC. All rights reserved.
+
+feelsgood, finnadie, goberserk, godmode, hurtrealbad, rage 1-4, suspect
+Copyright (c) 2013 id Software. All rights reserved.
+
+trollface
+Copyright (c) 2013 whynne@deviantart. All rights reserved.
+
+All other images
+Copyright (c) 2013 Apple Inc. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this
+software and associated documentation files (the "Software"), to deal in the Software
+without restriction, including without limitation the rights to use, copy, modify,
+merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be included in all copies
+or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+
+---
+
+This product contains a modified portion of 'marked', a full-featured markdown parser and compiler, written in JavaScript. Built for speed.
+
+by Christopher Jeffrey
+
+* HOMEPAGE:
+ * https://github.com/chjj/marked
+
+* LICENSE:
+
+Copyright (c) 2011-2014, Christopher Jeffrey (https://github.com/chjj/)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/api/context.go b/api/context.go
index 9a276a1a1..d90fbd9ee 100644
--- a/api/context.go
+++ b/api/context.go
@@ -456,18 +456,19 @@ func IsPrivateIpAddress(ipAddress string) bool {
}
func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request) {
+ props := make(map[string]string)
+ props["Message"] = err.Message
+ props["Details"] = err.DetailedError
- protocol := GetProtocol(r)
- SiteURL := protocol + "://" + r.Host
-
- m := make(map[string]string)
- m["Message"] = err.Message
- m["Details"] = err.DetailedError
- m["SiteName"] = utils.Cfg.TeamSettings.SiteName
- m["SiteURL"] = SiteURL
+ pathParts := strings.Split(r.URL.Path, "/")
+ if len(pathParts) > 1 {
+ props["SiteURL"] = GetProtocol(r) + "://" + r.Host + "/" + pathParts[1]
+ } else {
+ props["SiteURL"] = GetProtocol(r) + "://" + r.Host
+ }
w.WriteHeader(err.StatusCode)
- ServerTemplates.ExecuteTemplate(w, "error.html", m)
+ ServerTemplates.ExecuteTemplate(w, "error.html", Page{Props: props, ClientProps: utils.ClientProperties})
}
func Handle404(w http.ResponseWriter, r *http.Request) {
diff --git a/api/export.go b/api/export.go
index 73142a0e4..aff34073f 100644
--- a/api/export.go
+++ b/api/export.go
@@ -87,7 +87,7 @@ func ExportTeams(writer ExportWriter, options *ExportOptions) *model.AppError {
// Get the teams
var teams []*model.Team
if len(options.TeamsToExport) == 0 {
- if result := <-Srv.Store.Team().GetForExport(); result.Err != nil {
+ if result := <-Srv.Store.Team().GetAll(); result.Err != nil {
return result.Err
} else {
teams = result.Data.([]*model.Team)
diff --git a/api/file.go b/api/file.go
index 3606e406e..be8fc5456 100644
--- a/api/file.go
+++ b/api/file.go
@@ -13,6 +13,7 @@ import (
"github.com/gorilla/mux"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
+ "github.com/mssola/user_agent"
"github.com/rwcarlsen/goexif/exif"
_ "golang.org/x/image/bmp"
"image"
@@ -407,6 +408,19 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "max-age=2592000, public")
w.Header().Set("Content-Length", strconv.Itoa(len(f)))
w.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(filename)))
+
+ // attach extra headers to trigger a download on IE and Edge
+ ua := user_agent.New(r.UserAgent())
+ bname, _ := ua.Browser()
+
+ if bname == "Edge" || bname == "Internet Explorer" {
+ // trim off anything before the final / so we just get the file's name
+ parts := strings.Split(filename, "/")
+
+ w.Header().Set("Content-Type", "application/octet-stream")
+ w.Header().Set("Content-Disposition", "attachment;filename=\""+parts[len(parts)-1]+"\"")
+ }
+
w.Write(f)
}
diff --git a/api/import.go b/api/import.go
index e3f314d20..c465825b4 100644
--- a/api/import.go
+++ b/api/import.go
@@ -6,7 +6,6 @@ package api
import (
l4g "code.google.com/p/log4go"
"github.com/mattermost/platform/model"
- "github.com/mattermost/platform/utils"
)
//
@@ -24,9 +23,6 @@ func ImportPost(post *model.Post) {
func ImportUser(user *model.User) *model.User {
user.MakeNonNil()
- if len(user.Props["theme"]) == 0 {
- user.AddProp("theme", utils.Cfg.TeamSettings.DefaultThemeColor)
- }
if result := <-Srv.Store.User().Save(user); result.Err != nil {
l4g.Error("Error saving user. err=%v", result.Err)
diff --git a/api/team.go b/api/team.go
index c9d2412d3..4794b66df 100644
--- a/api/team.go
+++ b/api/team.go
@@ -25,12 +25,12 @@ func InitTeam(r *mux.Router) {
sr.Handle("/create_from_signup", ApiAppHandler(createTeamFromSignup)).Methods("POST")
sr.Handle("/create_with_sso/{service:[A-Za-z]+}", ApiAppHandler(createTeamFromSSO)).Methods("POST")
sr.Handle("/signup", ApiAppHandler(signupTeam)).Methods("POST")
+ sr.Handle("/all", ApiUserRequired(getAll)).Methods("GET")
sr.Handle("/find_team_by_name", ApiAppHandler(findTeamByName)).Methods("POST")
sr.Handle("/find_teams", ApiAppHandler(findTeams)).Methods("POST")
sr.Handle("/email_teams", ApiAppHandler(emailTeams)).Methods("POST")
sr.Handle("/invite_members", ApiUserRequired(inviteMembers)).Methods("POST")
sr.Handle("/update_name", ApiUserRequired(updateTeamDisplayName)).Methods("POST")
- sr.Handle("/update_valet_feature", ApiUserRequired(updateValetFeature)).Methods("POST")
sr.Handle("/me", ApiUserRequired(getMyTeam)).Methods("GET")
// These should be moved to the global admain console
sr.Handle("/import_team", ApiUserRequired(importTeam)).Methods("POST")
@@ -302,6 +302,53 @@ func isTreamCreationAllowed(c *Context, email string) bool {
return true
}
+func getAll(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !c.HasSystemAdminPermissions("getLogs") {
+ return
+ }
+
+ if result := <-Srv.Store.Team().GetAll(); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ teams := result.Data.([]*model.Team)
+ m := make(map[string]*model.Team)
+ for _, v := range teams {
+ m[v.Id] = v
+ }
+
+ w.Write([]byte(model.TeamMapToJson(m)))
+ }
+}
+
+func revokeAllSessions(c *Context, w http.ResponseWriter, r *http.Request) {
+ props := model.MapFromJson(r.Body)
+ id := props["id"]
+
+ if result := <-Srv.Store.Session().Get(id); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ session := result.Data.(*model.Session)
+
+ c.LogAudit("revoked_all=" + id)
+
+ if session.IsOAuth {
+ RevokeAccessToken(session.Token)
+ } else {
+ sessionCache.Remove(session.Token)
+
+ if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ w.Write([]byte(model.MapToJson(props)))
+ return
+ }
+ }
+ }
+}
+
func findTeamByName(c *Context, w http.ResponseWriter, r *http.Request) {
m := model.MapFromJson(r.Body)
@@ -533,52 +580,6 @@ func updateTeamDisplayName(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJson(props)))
}
-func updateValetFeature(c *Context, w http.ResponseWriter, r *http.Request) {
-
- props := model.MapFromJson(r.Body)
-
- allowValetStr := props["allow_valet"]
- if len(allowValetStr) == 0 {
- c.SetInvalidParam("updateValetFeature", "allow_valet")
- return
- }
-
- teamId := props["team_id"]
- if len(teamId) > 0 && len(teamId) != 26 {
- c.SetInvalidParam("updateValetFeature", "team_id")
- return
- } else if len(teamId) == 0 {
- teamId = c.Session.TeamId
- }
-
- tchan := Srv.Store.Team().Get(teamId)
-
- if !c.HasPermissionsToTeam(teamId, "updateValetFeature") {
- return
- }
-
- if !model.IsInRole(c.Session.Roles, model.ROLE_TEAM_ADMIN) {
- c.Err = model.NewAppError("updateValetFeature", "You do not have the appropriate permissions", "userId="+c.Session.UserId)
- c.Err.StatusCode = http.StatusForbidden
- return
- }
-
- var team *model.Team
- if tResult := <-tchan; tResult.Err != nil {
- c.Err = tResult.Err
- return
- } else {
- team = tResult.Data.(*model.Team)
- }
-
- if result := <-Srv.Store.Team().Update(team); result.Err != nil {
- c.Err = result.Err
- return
- }
-
- w.Write([]byte(model.MapToJson(props)))
-}
-
func getMyTeam(c *Context, w http.ResponseWriter, r *http.Request) {
if len(c.Session.TeamId) == 0 {
diff --git a/api/team_test.go b/api/team_test.go
index cd39dacfe..e2a7cf430 100644
--- a/api/team_test.go
+++ b/api/team_test.go
@@ -132,6 +132,39 @@ func TestFindTeamByEmail(t *testing.T) {
}
}
+func TestGetAllTeams(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")
+
+ if _, err := Client.GetAllTeams(); err == nil {
+ t.Fatal("you shouldn't have permissions")
+ }
+
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ if r1, err := Client.GetAllTeams(); err != nil {
+ t.Fatal(err)
+ } else {
+ teams := r1.Data.(map[string]*model.Team)
+ if teams[team.Id].Name != team.Name {
+ t.Fatal()
+ }
+ }
+}
+
/*
XXXXXX investigate and fix failing test
diff --git a/api/templates/error.html b/api/templates/error.html
index 760578896..6b643556e 100644
--- a/api/templates/error.html
+++ b/api/templates/error.html
@@ -23,7 +23,7 @@
<div class="error__container">
<div class="error__icon"><i class="fa fa-exclamation-triangle"></i></div>
<h2>{{ .ClientProps.SiteName }} needs your help:</h2>
- <p>{{.Message}}</p>
+ <p>{{ .Props.Message }}</p>
<a href="{{.Props.SiteURL}}">Go back to team site</a>
</div>
</div>
diff --git a/api/templates/signup_team_body.html b/api/templates/signup_team_body.html
index f5c0e62b0..e6ffb3a5b 100644
--- a/api/templates/signup_team_body.html
+++ b/api/templates/signup_team_body.html
@@ -22,9 +22,6 @@
<a href="{{.Props.Link}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Set up your team</a>
</p>
{{ .ClientProps.SiteName }} is one place for all your team communication, searchable and available anywhere.<br>You'll get more out of {{ .ClientProps.SiteName }} when your team is in constant communication--let's get them on board.<br></p>
- <p>
- Learn more by <a href="{{.Props.TourUrl}}" style="text-decoration: none; color:#2389D7;">taking a tour</a>
- </p>
</td>
</tr>
<tr>
diff --git a/api/templates/welcome_body.html b/api/templates/welcome_body.html
index c75e14c6a..5fe3450b7 100644
--- a/api/templates/welcome_body.html
+++ b/api/templates/welcome_body.html
@@ -17,15 +17,9 @@
<table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto">
<tr>
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
- <h2 style="font-weight: normal; margin-top: 10px;">You joined the {{.Props.TeamDisplayName}} team at {{.ClientProps.SiteName}}!</h2>
- <p>Please let me know if you have any questions.<br>Enjoy your stay at <a href="{{.Props.TeamURL}}">{{.ClientProps.SiteName}}</a>.</p>
- </td>
- </tr>
- <tr>
- <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;">
- Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br>
- Best wishes,<br>
- The {{.ClientProps.SiteName}} Team<br>
+ <h2 style="font-weight: normal; margin-top: 10px;">You can sign-in to your new team from the web address:</h2>
+ <a href="{{.Props.TeamURL}}">{{.Props.TeamURL}}</a>
+ <p>Mattermost lets you share messages and files from your PC or phone, with instant search and archiving.</p>
</td>
</tr>
</table>
diff --git a/api/templates/welcome_subject.html b/api/templates/welcome_subject.html
index 2214f7a38..c3b70ef20 100644
--- a/api/templates/welcome_subject.html
+++ b/api/templates/welcome_subject.html
@@ -1 +1 @@
-{{define "welcome_subject"}}Welcome to {{ .ClientProps.SiteName }}{{end}} \ No newline at end of file
+{{define "welcome_subject"}}You joined {{ .Props.TeamDisplayName }}{{end}}
diff --git a/api/user.go b/api/user.go
index 3f26531ad..695ab2208 100644
--- a/api/user.go
+++ b/api/user.go
@@ -51,6 +51,7 @@ func InitUser(r *mux.Router) {
sr.Handle("/me", ApiAppHandler(getMe)).Methods("GET")
sr.Handle("/status", ApiUserRequiredActivity(getStatuses, false)).Methods("GET")
sr.Handle("/profiles", ApiUserRequired(getProfiles)).Methods("GET")
+ sr.Handle("/profiles/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfiles)).Methods("GET")
sr.Handle("/{id:[A-Za-z0-9]+}", ApiUserRequired(getUser)).Methods("GET")
sr.Handle("/{id:[A-Za-z0-9]+}/sessions", ApiUserRequired(getSessions)).Methods("GET")
sr.Handle("/{id:[A-Za-z0-9]+}/audits", ApiUserRequired(getAudits)).Methods("GET")
@@ -166,6 +167,19 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User {
if team.Email == user.Email {
user.Roles = model.ROLE_TEAM_ADMIN
channelRole = model.CHANNEL_ROLE_ADMIN
+
+ // Below is a speical case where the first user in the entire
+ // system is granted the system_admin role instead of admin
+ if result := <-Srv.Store.User().GetTotalUsersCount(); result.Err != nil {
+ c.Err = result.Err
+ return nil
+ } else {
+ count := result.Data.(int64)
+ if count <= 0 {
+ user.Roles = model.ROLE_SYSTEM_ADMIN
+ }
+ }
+
} else {
user.Roles = ""
}
@@ -184,7 +198,7 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User {
l4g.Error("Encountered an issue joining default channels user_id=%s, team_id=%s, err=%v", ruser.Id, ruser.TeamId, err)
}
- //fireAndForgetWelcomeEmail(ruser.FirstName, ruser.Email, team.Name, c.TeamURL+"/channels/town-square")
+ fireAndForgetWelcomeEmail(ruser.Email, team.DisplayName, c.GetTeamURLFromTeam(team))
if user.EmailVerified {
if cresult := <-Srv.Store.User().VerifyEmail(ruser.Id); cresult.Err != nil {
l4g.Error("Failed to set email verified err=%v", cresult.Err)
@@ -204,17 +218,13 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User {
}
}
-func fireAndForgetWelcomeEmail(name, email, teamDisplayName, link, siteURL string) {
+func fireAndForgetWelcomeEmail(email, teamDisplayName, teamURL string) {
go func() {
subjectPage := NewServerTemplatePage("welcome_subject")
- subjectPage.Props["SiteURL"] = siteURL
+ subjectPage.Props["TeamDisplayName"] = teamDisplayName
bodyPage := NewServerTemplatePage("welcome_body")
- bodyPage.Props["SiteURL"] = siteURL
- bodyPage.Props["Nickname"] = name
- bodyPage.Props["TeamDisplayName"] = teamDisplayName
- bodyPage.Props["FeedbackName"] = utils.Cfg.EmailSettings.FeedbackName
- bodyPage.Props["TeamURL"] = link
+ bodyPage.Props["TeamURL"] = teamURL
if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil {
l4g.Error("Failed to send welcome email successfully err=%v", err)
@@ -550,13 +560,26 @@ func getUser(c *Context, w http.ResponseWriter, r *http.Request) {
}
func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+ id, ok := params["id"]
+ if ok {
+ // You must be system admin to access another team
+ if id != c.Session.TeamId {
+ if !c.HasSystemAdminPermissions("getProfiles") {
+ return
+ }
+ }
+
+ } else {
+ id = c.Session.TeamId
+ }
- etag := (<-Srv.Store.User().GetEtagForProfiles(c.Session.TeamId)).Data.(string)
+ etag := (<-Srv.Store.User().GetEtagForProfiles(id)).Data.(string)
if HandleEtag(etag, w, r) {
return
}
- if result := <-Srv.Store.User().GetProfiles(c.Session.TeamId); result.Err != nil {
+ if result := <-Srv.Store.User().GetProfiles(id); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -794,6 +817,9 @@ func uploadProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
Srv.Store.User().UpdateLastPictureUpdate(c.Session.UserId)
c.LogAudit("")
+
+ // write something as the response since jQuery expects a json response
+ w.Write([]byte("true"))
}
func updateUser(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -924,8 +950,8 @@ func updateRoles(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if model.IsInRole(new_roles, model.ROLE_SYSTEM_ADMIN) {
- c.Err = model.NewAppError("updateRoles", "The system_admin role can only be set from the command line", "")
+ if model.IsInRole(new_roles, model.ROLE_SYSTEM_ADMIN) && !c.IsSystemAdmin() {
+ c.Err = model.NewAppError("updateRoles", "The system_admin role can only be set by another system admin", "")
c.Err.StatusCode = http.StatusForbidden
return
}
@@ -1155,29 +1181,35 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- hash := props["hash"]
- if len(hash) == 0 {
- c.SetInvalidParam("resetPassword", "hash")
+ name := props["name"]
+ if len(name) == 0 {
+ c.SetInvalidParam("resetPassword", "name")
return
}
- data := model.MapFromJson(strings.NewReader(props["data"]))
+ userId := props["user_id"]
+ hash := props["hash"]
+ timeStr := ""
- userId := data["user_id"]
- if len(userId) != 26 {
- c.SetInvalidParam("resetPassword", "data:user_id")
- return
- }
+ if !c.IsSystemAdmin() {
+ if len(hash) == 0 {
+ c.SetInvalidParam("resetPassword", "hash")
+ return
+ }
- timeStr := data["time"]
- if len(timeStr) == 0 {
- c.SetInvalidParam("resetPassword", "data:time")
- return
+ data := model.MapFromJson(strings.NewReader(props["data"]))
+
+ userId = data["user_id"]
+
+ timeStr = data["time"]
+ if len(timeStr) == 0 {
+ c.SetInvalidParam("resetPassword", "data:time")
+ return
+ }
}
- name := props["name"]
- if len(name) == 0 {
- c.SetInvalidParam("resetPassword", "name")
+ if len(userId) != 26 {
+ c.SetInvalidParam("resetPassword", "user_id")
return
}
@@ -1205,15 +1237,17 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", props["data"], utils.Cfg.EmailSettings.PasswordResetSalt)) {
- c.Err = model.NewAppError("resetPassword", "The reset password link does not appear to be valid", "")
- return
- }
+ if !c.IsSystemAdmin() {
+ if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", props["data"], utils.Cfg.EmailSettings.PasswordResetSalt)) {
+ c.Err = model.NewAppError("resetPassword", "The reset password link does not appear to be valid", "")
+ return
+ }
- t, err := strconv.ParseInt(timeStr, 10, 64)
- if err != nil || model.GetMillis()-t > 1000*60*60 { // one hour
- c.Err = model.NewAppError("resetPassword", "The reset link has expired", "")
- return
+ t, err := strconv.ParseInt(timeStr, 10, 64)
+ if err != nil || model.GetMillis()-t > 1000*60*60 { // one hour
+ c.Err = model.NewAppError("resetPassword", "The reset link has expired", "")
+ return
+ }
}
if result := <-Srv.Store.User().UpdatePassword(userId, model.HashPassword(newPassword)); result.Err != nil {
diff --git a/api/user_test.go b/api/user_test.go
index 669da4d20..baa567dec 100644
--- a/api/user_test.go
+++ b/api/user_test.go
@@ -228,6 +228,13 @@ func TestGetUser(t *testing.T) {
ruser2, _ := Client.CreateUser(&user2, "")
store.Must(Srv.Store.User().VerifyEmail(ruser2.Data.(*model.User).Id))
+ team2 := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ rteam2, _ := Client.CreateTeam(&team2)
+
+ user3 := model.User{TeamId: rteam2.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ ruser3, _ := Client.CreateUser(&user3, "")
+ store.Must(Srv.Store.User().VerifyEmail(ruser3.Data.(*model.User).Id))
+
Client.LoginByEmail(team.Name, user.Email, user.Password)
rId := ruser.Data.(*model.User).Id
@@ -276,13 +283,27 @@ func TestGetUser(t *testing.T) {
t.Log(cache_result.Data)
t.Fatal("cache should be empty")
}
+ }
+ if _, err := Client.GetProfiles(rteam2.Data.(*model.Team).Id, ""); err == nil {
+ t.Fatal("shouldn't have access")
}
Client.AuthToken = ""
if _, err := Client.GetUser(ruser2.Data.(*model.User).Id, ""); err == nil {
t.Fatal("shouldn't have accss")
}
+
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, ruser.Data.(*model.User), model.ROLE_SYSTEM_ADMIN)
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ if _, err := Client.GetProfiles(rteam2.Data.(*model.Team).Id, ""); err != nil {
+ t.Fatal(err)
+ }
}
func TestGetAudits(t *testing.T) {
diff --git a/config/config.json b/config/config.json
index aa92ccf4e..60e197154 100644
--- a/config/config.json
+++ b/config/config.json
@@ -11,7 +11,6 @@
"TeamSettings": {
"SiteName": "Mattermost",
"MaxUsersPerTeam": 50,
- "DefaultThemeColor": "#2389D7",
"EnableTeamCreation": true,
"EnableUserCreation": true,
"RestrictCreationToDomains": ""
diff --git a/doc/README.md b/doc/README.md
index 6bf96492a..7713c40ab 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -23,3 +23,7 @@
- [Code Contribution Guidelines](developer/code-contribution.md)
- [Developer Machine Setup](install/dev-setup.md)
- [Mattermost Style Guide](developer/style-guide.md)
+
+## End User Help
+
+- [Mattermost Markdown Formatting](help/enduser/markdown.md)
diff --git a/doc/help/enduser/markdown.md b/doc/help/enduser/markdown.md
new file mode 100644
index 000000000..9e2342a0b
--- /dev/null
+++ b/doc/help/enduser/markdown.md
@@ -0,0 +1,152 @@
+# Markdown Help
+
+Markdown makes it easy to format messages. Type a message as you normally would, and use these rules to render it with special formatting.
+
+## Text Style:
+
+You can use either `_` or `*` around a word to make it italic. Use two to make it bold.
+
+* `_italics_` renders as _italics_
+* `**bold**` renders as **bold**
+* `**_bold-italic_**` renders as **_bold-italics_**
+* `~~strikethrough~~` renders as ~~strikethrough~~
+
+## Code:
+
+Create a code block by indenting four spaces, or by placing ``` on the line above and below your code.
+
+Example:
+
+ ```
+ code block
+ ```
+
+Renders as:
+```
+code block
+```
+
+Create in-line monospaced font by surrounding it with back spaces.
+```
+`monospace`
+```
+Renders as: `monospace`.
+
+## Links:
+
+Create labeled links by putting the desired text in square brackets and the associated link in normal brackets.
+
+`[Check out Mattermost!](www.mattermost.com)`
+
+Renders as: [Check out Mattermost!](www.mattermost.com)
+
+## In-line Images
+
+Create in-line images using an `!` followed by the alt text in square brackets and the link in normal brackets. Add hover text by placing it in quotes after the link.
+```
+![alt text](link "hover text")
+
+and
+
+[![Build Status](https://travis-ci.org/mattermost/platform.svg?branch=master)](https://travis-ci.org/mattermost/platform) [![Github](https://assets-cdn.github.com/favicon.ico)](https://github.com/mattermost/platform)
+```
+Renders as:
+
+![alt text](link "hover text")
+
+and
+
+[![Build Status](https://travis-ci.org/mattermost/platform.svg?branch=master)](https://travis-ci.org/mattermost/platform) [![Github](https://assets-cdn.github.com/favicon.ico)](https://github.com/mattermost/platform)
+
+## Emojis
+
+Check out a full list of emojis [here](http://www.emoji-cheat-sheet.com/).
+
+```
+:smile: :+1: :sheep:
+```
+Renders as:
+:smile: :+1: :sheep:
+
+## Lines:
+
+Create a line by using three `*`, `_`, or `-`.
+
+`***` renders as:
+***
+
+## Block quotes:
+
+Create block quotes using `>`.
+
+`> block quotes` renders as:
+> block quotes
+
+## Lists:
+
+Create a list by using `*` or `-` as bullets. Indent a bullet point by adding two spaces in front of it.
+```
+* list item one
+* list item two
+ * item two sub-point
+```
+Renders as:
+* list item one
+* list item two
+ * item two sub-point
+
+Make it an ordered list by using numbers instead:
+```
+1. Item one
+2. Item two
+```
+Renders as:
+1. Item one
+2. Item two
+
+## Tables:
+
+Create a table by placing a dashed line under the header row and separating the columns with a pipe `|`. (The columns don’t need to line up exactly for it to work). Choose how to align table columns by including colons `:` within the header row.
+```
+| Left-Aligned  | Center Aligned  | Right Aligned |
+| :------------ |:---------------:| -----:|
+| Left column 1 | this text       |  $100 |
+| Left column 2 | is              |   $10 |
+| Left column 3 | centered        |    $1 |
+```
+
+Renders as:
+
+| Left-Aligned  | Center Aligned  | Right Aligned |
+| :------------ |:---------------:| -----:|
+| Left column 1 | this text       |  $100 |
+| Left column 2 | is              |   $10 |
+| Left column 3 | centered        |    $1 |
+
+## Headings:
+
+Make a heading by typing # and a space before your title. For smaller headings, use more #’s.
+```
+# Large heading
+## Smaller heading
+### Even smaller heading
+```
+Renders as:
+# Large Heading
+## Smaller Heading
+### Even smaller heading
+
+Alternatively, for the large heading you can underline the text using `===`. For the smaller heading you can underline using `---`
+```
+Large Heading
+=============
+
+Smaller Heading
+--------------
+```
+Renders as:
+Large Heading
+=============
+
+Smaller Heading
+--------------
diff --git a/doc/install/single-container-install.md b/doc/install/single-container-install.md
index 772f3becf..fa5265773 100644
--- a/doc/install/single-container-install.md
+++ b/doc/install/single-container-install.md
@@ -4,11 +4,11 @@ The following install instructions are for single-container installs of Mattermo
### Mac OSX ###
-1. Install Boot2Docker using instructions at: http://docs.docker.com/installation/mac/
- 1. Start Boot2Docker from the command line and run: `boot2docker init eval “$(boot2docker shellinit)”`
-2. Get your Docker IP address with: `boot2docker ip`
+1. Install Docker Toolbox using instructions at: http://docs.docker.com/installation/mac/
+ 1. Start Docker Toolbox from the command line and run: `docker-machine create -d virtualbox dev”`
+2. Get your Docker IP address with: `docker-machine ip dev`
3. Use `sudo nano /etc/hosts` to add `<Docker IP> dockerhost` to your /etc/hosts file
-4. Run: `boot2docker shellinit` and copy the export statements to your ~/.bash\_profile by running `sudo nano ~/.bash_profile`. Then run: `source ~/.bash_profile`
+4. Run: `docker-machine env dev` and copy the export statements to your ~/.bash\_profile by running `sudo nano ~/.bash_profile`. Then run: `source ~/.bash_profile`
5. Run: `docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform`
6. When docker is done fetching the image, open http://dockerhost:8065/ in your browser.
diff --git a/docker/dev/config_docker.json b/docker/dev/config_docker.json
index 16a4007fa..06fee9bd5 100644
--- a/docker/dev/config_docker.json
+++ b/docker/dev/config_docker.json
@@ -11,7 +11,6 @@
"TeamSettings": {
"SiteName": "Mattermost",
"MaxUsersPerTeam": 50,
- "DefaultThemeColor": "#2389D7",
"EnableTeamCreation": true,
"EnableUserCreation": true,
"RestrictCreationToDomains": ""
diff --git a/docker/local/config_docker.json b/docker/local/config_docker.json
index 16a4007fa..06fee9bd5 100644
--- a/docker/local/config_docker.json
+++ b/docker/local/config_docker.json
@@ -11,7 +11,6 @@
"TeamSettings": {
"SiteName": "Mattermost",
"MaxUsersPerTeam": 50,
- "DefaultThemeColor": "#2389D7",
"EnableTeamCreation": true,
"EnableUserCreation": true,
"RestrictCreationToDomains": ""
diff --git a/mattermost.go b/mattermost.go
index 4608bfff3..ff7b0f3f3 100644
--- a/mattermost.go
+++ b/mattermost.go
@@ -325,10 +325,10 @@ Usage:
-role="admin" The role used in other commands
valid values are
- "" - The empty role is basic user
+ "" - The empty role is basic user
permissions
"admin" - Represents a team admin and
- is used to help adminsiter one team.
+ is used to help administer one team.
"system_admin" - Represents a system
admin who has access to all teams
and configuration settings. This
diff --git a/model/client.go b/model/client.go
index cc75ce370..26e00864d 100644
--- a/model/client.go
+++ b/model/client.go
@@ -150,6 +150,16 @@ func (c *Client) CreateTeam(team *Team) (*Result, *AppError) {
}
}
+func (c *Client) GetAllTeams() (*Result, *AppError) {
+ if r, err := c.DoApiGet("/teams/all", "", ""); err != nil {
+ return nil, err
+ } else {
+
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), TeamMapFromJson(r.Body)}, nil
+ }
+}
+
func (c *Client) FindTeamByName(name string, allServers bool) (*Result, *AppError) {
m := make(map[string]string)
m["name"] = name
@@ -208,15 +218,6 @@ func (c *Client) UpdateTeamDisplayName(data map[string]string) (*Result, *AppErr
}
}
-func (c *Client) UpdateValetFeature(data map[string]string) (*Result, *AppError) {
- if r, err := c.DoApiPost("/teams/update_valet_feature", MapToJson(data)); err != nil {
- return nil, err
- } else {
- return &Result{r.Header.Get(HEADER_REQUEST_ID),
- r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
- }
-}
-
func (c *Client) CreateUser(user *User, hash string) (*Result, *AppError) {
if r, err := c.DoApiPost("/users/create", user.ToJson()); err != nil {
return nil, err
@@ -254,7 +255,7 @@ func (c *Client) GetMe(etag string) (*Result, *AppError) {
}
func (c *Client) GetProfiles(teamId string, etag string) (*Result, *AppError) {
- if r, err := c.DoApiGet("/users/profiles", "", etag); err != nil {
+ if r, err := c.DoApiGet("/users/profiles/"+teamId, "", etag); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -570,15 +571,6 @@ func (c *Client) CreatePost(post *Post) (*Result, *AppError) {
}
}
-func (c *Client) CreateValetPost(post *Post) (*Result, *AppError) {
- if r, err := c.DoApiPost("/channels/"+post.ChannelId+"/valet_create", post.ToJson()); err != nil {
- return nil, err
- } else {
- return &Result{r.Header.Get(HEADER_REQUEST_ID),
- r.Header.Get(HEADER_ETAG_SERVER), PostFromJson(r.Body)}, nil
- }
-}
-
func (c *Client) UpdatePost(post *Post) (*Result, *AppError) {
if r, err := c.DoApiPost("/channels/"+post.ChannelId+"/update", post.ToJson()); err != nil {
return nil, err
diff --git a/model/config.go b/model/config.go
index e711f9522..853e2bbc0 100644
--- a/model/config.go
+++ b/model/config.go
@@ -112,7 +112,6 @@ type PrivacySettings struct {
type TeamSettings struct {
SiteName string
MaxUsersPerTeam int
- DefaultThemeColor string
EnableTeamCreation bool
EnableUserCreation bool
RestrictCreationToDomains string
diff --git a/model/team.go b/model/team.go
index 0d740dde2..f80fa3b11 100644
--- a/model/team.go
+++ b/model/team.go
@@ -73,6 +73,26 @@ func TeamFromJson(data io.Reader) *Team {
}
}
+func TeamMapToJson(u map[string]*Team) string {
+ b, err := json.Marshal(u)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func TeamMapFromJson(data io.Reader) map[string]*Team {
+ decoder := json.NewDecoder(data)
+ var teams map[string]*Team
+ err := decoder.Decode(&teams)
+ if err == nil {
+ return teams
+ } else {
+ return nil
+ }
+}
+
func (o *Team) Etag() string {
return Etag(o.Id, o.UpdateAt)
}
diff --git a/model/user.go b/model/user.go
index 3a2c9d56c..5cb774478 100644
--- a/model/user.go
+++ b/model/user.go
@@ -23,7 +23,6 @@ const (
USER_NOTIFY_ALL = "all"
USER_NOTIFY_MENTION = "mention"
USER_NOTIFY_NONE = "none"
- BOT_USERNAME = "valet"
)
type User struct {
diff --git a/store/sql_session_store.go b/store/sql_session_store.go
index c1d2c852b..22411389d 100644
--- a/store/sql_session_store.go
+++ b/store/sql_session_store.go
@@ -140,6 +140,24 @@ func (me SqlSessionStore) Remove(sessionIdOrToken string) StoreChannel {
return storeChannel
}
+func (me SqlSessionStore) RemoveAllSessionsForTeam(teamId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ _, err := me.GetMaster().Exec("DELETE FROM Sessions WHERE TeamId = :TeamId", map[string]interface{}{"TeamId": teamId})
+ if err != nil {
+ result.Err = model.NewAppError("SqlSessionStore.RemoveAllSessionsForTeam", "We couldn't remove all the sessions for the team", "id="+teamId+", err="+err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
func (me SqlSessionStore) CleanUpExpiredSessions(userId string) StoreChannel {
storeChannel := make(StoreChannel)
diff --git a/store/sql_session_store_test.go b/store/sql_session_store_test.go
index 4ae680556..3d8aafe25 100644
--- a/store/sql_session_store_test.go
+++ b/store/sql_session_store_test.go
@@ -80,6 +80,29 @@ func TestSessionRemove(t *testing.T) {
}
}
+func TestSessionRemoveAll(t *testing.T) {
+ Setup()
+
+ s1 := model.Session{}
+ s1.UserId = model.NewId()
+ s1.TeamId = model.NewId()
+ Must(store.Session().Save(&s1))
+
+ if rs1 := (<-store.Session().Get(s1.Id)); rs1.Err != nil {
+ t.Fatal(rs1.Err)
+ } else {
+ if rs1.Data.(*model.Session).Id != s1.Id {
+ t.Fatal("should match")
+ }
+ }
+
+ Must(store.Session().RemoveAllSessionsForTeam(s1.TeamId))
+
+ if rs2 := (<-store.Session().Get(s1.Id)); rs2.Err == nil {
+ t.Fatal("should have been removed")
+ }
+}
+
func TestSessionRemoveToken(t *testing.T) {
Setup()
diff --git a/store/sql_team_store.go b/store/sql_team_store.go
index 3d644e577..109fe5401 100644
--- a/store/sql_team_store.go
+++ b/store/sql_team_store.go
@@ -196,7 +196,7 @@ func (s SqlTeamStore) GetTeamsForEmail(email string) StoreChannel {
return storeChannel
}
-func (s SqlTeamStore) GetForExport() StoreChannel {
+func (s SqlTeamStore) GetAll() StoreChannel {
storeChannel := make(StoreChannel)
go func() {
@@ -204,7 +204,7 @@ func (s SqlTeamStore) GetForExport() StoreChannel {
var data []*model.Team
if _, err := s.GetReplica().Select(&data, "SELECT * FROM Teams"); err != nil {
- result.Err = model.NewAppError("SqlTeamStore.GetForExport", "We could not get all teams", err.Error())
+ result.Err = model.NewAppError("SqlTeamStore.GetAllTeams", "We could not get all teams", err.Error())
}
result.Data = data
diff --git a/store/sql_user_store.go b/store/sql_user_store.go
index 3fd1c82b5..0a723d965 100644
--- a/store/sql_user_store.go
+++ b/store/sql_user_store.go
@@ -481,3 +481,22 @@ func (us SqlUserStore) GetForExport(teamId string) StoreChannel {
return storeChannel
}
+
+func (us SqlUserStore) GetTotalUsersCount() StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if count, err := us.GetReplica().SelectInt("SELECT COUNT(Id) FROM Users"); err != nil {
+ result.Err = model.NewAppError("SqlUserStore.GetTotalUsersCount", "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 466da2845..e2a454023 100644
--- a/store/sql_user_store_test.go
+++ b/store/sql_user_store_test.go
@@ -206,6 +206,24 @@ func TestUserStoreGet(t *testing.T) {
}
}
+func TestUserCountt(t *testing.T) {
+ Setup()
+
+ u1 := model.User{}
+ u1.TeamId = model.NewId()
+ u1.Email = model.NewId()
+ Must(store.User().Save(&u1))
+
+ if result := <-store.User().GetTotalUsersCount(); 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 c9d40cfa5..23580f452 100644
--- a/store/store.go
+++ b/store/store.go
@@ -47,7 +47,7 @@ type TeamStore interface {
Get(id string) StoreChannel
GetByName(name string) StoreChannel
GetTeamsForEmail(domain string) StoreChannel
- GetForExport() StoreChannel
+ GetAll() StoreChannel
}
type ChannelStore interface {
@@ -103,6 +103,7 @@ type UserStore interface {
GetEtagForProfiles(teamId string) StoreChannel
UpdateFailedPasswordAttempts(userId string, attempts int) StoreChannel
GetForExport(teamId string) StoreChannel
+ GetTotalUsersCount() StoreChannel
}
type SessionStore interface {
@@ -110,6 +111,7 @@ type SessionStore interface {
Get(sessionIdOrToken string) StoreChannel
GetSessions(userId string) StoreChannel
Remove(sessionIdOrToken string) StoreChannel
+ RemoveAllSessionsForTeam(teamId string) StoreChannel
UpdateLastActivityAt(sessionId string, time int64) StoreChannel
UpdateRoles(userId string, roles string) StoreChannel
}
diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx
index 7cbd4021e..fe40385a0 100644
--- a/web/react/components/activity_log_modal.jsx
+++ b/web/react/components/activity_log_modal.jsx
@@ -25,7 +25,8 @@ export default class ActivityLogModal extends React.Component {
clientError: null
};
}
- submitRevoke(altId) {
+ submitRevoke(altId, e) {
+ e.preventDefault();
Client.revokeSession(altId,
function handleRevokeSuccess() {
AsyncClient.getSessions();
@@ -86,7 +87,7 @@ export default class ActivityLogModal extends React.Component {
<div>{`First time active: ${firstAccessTime.toDateString()}, ${lastAccessTime.toLocaleTimeString()}`}</div>
<div>{`OS: ${currentSession.props.os}`}</div>
<div>{`Browser: ${currentSession.props.browser}`}</div>
- <div>{`Session ID: ${currentSession.alt_id}`}</div>
+ <div>{`Session ID: ${currentSession.id}`}</div>
</div>
);
} else {
@@ -115,7 +116,7 @@ export default class ActivityLogModal extends React.Component {
</div>
<div className='activity-log__action'>
<button
- onClick={this.submitRevoke.bind(this, currentSession.alt_id)}
+ onClick={this.submitRevoke.bind(this, currentSession.id)}
className='btn btn-primary'
>
Logout
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index 6fddfef07..92f0bbdce 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -3,6 +3,7 @@
var AdminSidebar = require('./admin_sidebar.jsx');
var AdminStore = require('../../stores/admin_store.jsx');
+var TeamStore = require('../../stores/team_store.jsx');
var AsyncClient = require('../../utils/async_client.jsx');
var LoadingScreen = require('../loading_screen.jsx');
@@ -16,38 +17,104 @@ var GitLabSettingsTab = require('./gitlab_settings.jsx');
var SqlSettingsTab = require('./sql_settings.jsx');
var TeamSettingsTab = require('./team_settings.jsx');
var ServiceSettingsTab = require('./service_settings.jsx');
+var TeamUsersTab = require('./team_users.jsx');
export default class AdminController extends React.Component {
constructor(props) {
super(props);
this.selectTab = this.selectTab.bind(this);
+ this.removeSelectedTeam = this.removeSelectedTeam.bind(this);
+ this.addSelectedTeam = this.addSelectedTeam.bind(this);
this.onConfigListenerChange = this.onConfigListenerChange.bind(this);
+ this.onAllTeamsListenerChange = this.onAllTeamsListenerChange.bind(this);
+
+ var selectedTeams = AdminStore.getSelectedTeams();
+ if (selectedTeams == null) {
+ selectedTeams = {};
+ selectedTeams[TeamStore.getCurrentId()] = 'true';
+ AdminStore.saveSelectedTeams(selectedTeams);
+ }
this.state = {
- config: null,
- selected: 'service_settings'
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams,
+ selected: 'service_settings',
+ selectedTeam: null
};
}
componentDidMount() {
AdminStore.addConfigChangeListener(this.onConfigListenerChange);
AsyncClient.getConfig();
+
+ AdminStore.addAllTeamsChangeListener(this.onAllTeamsListenerChange);
+ AsyncClient.getAllTeams();
}
componentWillUnmount() {
AdminStore.removeConfigChangeListener(this.onConfigListenerChange);
+ AdminStore.removeAllTeamsChangeListener(this.onAllTeamsListenerChange);
}
onConfigListenerChange() {
this.setState({
config: AdminStore.getConfig(),
- selected: this.state.selected
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
});
}
- selectTab(tab) {
- this.setState({selected: tab});
+ onAllTeamsListenerChange() {
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
+
+ });
+ }
+
+ selectTab(tab, teamId) {
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: tab,
+ selectedTeam: teamId
+ });
+ }
+
+ removeSelectedTeam(teamId) {
+ var selectedTeams = AdminStore.getSelectedTeams();
+ Reflect.deleteProperty(selectedTeams, teamId);
+ AdminStore.saveSelectedTeams(selectedTeams);
+
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
+ });
+ }
+
+ addSelectedTeam(teamId) {
+ var selectedTeams = AdminStore.getSelectedTeams();
+ selectedTeams[teamId] = 'true';
+ AdminStore.saveSelectedTeams(selectedTeams);
+
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
+ });
}
render() {
@@ -74,6 +141,8 @@ export default class AdminController extends React.Component {
tab = <TeamSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'service_settings') {
tab = <ServiceSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'team_users') {
+ tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />;
}
}
@@ -85,7 +154,12 @@ export default class AdminController extends React.Component {
/>
<AdminSidebar
selected={this.state.selected}
+ selectedTeam={this.state.selectedTeam}
selectTab={this.selectTab}
+ teams={this.state.teams}
+ selectedTeams={this.state.selectedTeams}
+ removeSelectedTeam={this.removeSelectedTeam}
+ addSelectedTeam={this.addSelectedTeam}
/>
<div className='inner__wrap channel__wrap'>
<div className='row header'>
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
index 17ce39c7c..4b9ff3cb8 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
var AdminSidebarHeader = require('./admin_sidebar_header.jsx');
+var SelectTeamModal = require('./select_team_modal.jsx');
export default class AdminSidebar extends React.Component {
constructor(props) {
@@ -9,28 +10,121 @@ export default class AdminSidebar extends React.Component {
this.isSelected = this.isSelected.bind(this);
this.handleClick = this.handleClick.bind(this);
+ this.removeTeam = this.removeTeam.bind(this);
+
+ this.showTeamSelect = this.showTeamSelect.bind(this);
+ this.teamSelectedModal = this.teamSelectedModal.bind(this);
+ this.teamSelectedModalDismissed = this.teamSelectedModalDismissed.bind(this);
this.state = {
+ showSelectModal: false
};
}
- handleClick(name, e) {
+ handleClick(name, teamId, e) {
e.preventDefault();
- this.props.selectTab(name);
+ this.props.selectTab(name, teamId);
}
- isSelected(name) {
+ isSelected(name, teamId) {
if (this.props.selected === name) {
- return 'active';
+ if (name === 'team_users') {
+ if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) {
+ return 'active';
+ }
+ } else {
+ return 'active';
+ }
}
return '';
}
+ removeTeam(teamId, e) {
+ e.preventDefault();
+ Reflect.deleteProperty(this.props.selectedTeams, teamId);
+ this.props.removeSelectedTeam(teamId);
+
+ if (this.props.selected === 'team_users') {
+ if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) {
+ this.props.selectTab('service_settings', null);
+ }
+ }
+ }
+
componentDidMount() {
}
+ showTeamSelect(e) {
+ e.preventDefault();
+ this.setState({showSelectModal: true});
+ }
+
+ teamSelectedModal(teamId) {
+ this.props.selectedTeams[teamId] = 'true';
+ this.setState({showSelectModal: false});
+ this.props.addSelectedTeam(teamId);
+ this.forceUpdate();
+ }
+
+ teamSelectedModalDismissed() {
+ this.setState({showSelectModal: false});
+ }
+
render() {
+ var count = '*';
+ var teams = 'Loading';
+
+ if (this.props.teams != null) {
+ count = '' + Object.keys(this.props.teams).length;
+
+ teams = [];
+ for (var key in this.props.selectedTeams) {
+ if (this.props.selectedTeams.hasOwnProperty(key)) {
+ var team = this.props.teams[key];
+
+ if (team != null) {
+ teams.push(
+ <ul
+ key={'team_' + team.id}
+ className='nav nav__sub-menu'
+ >
+ <li>
+ <a
+ href='#'
+ onClick={this.handleClick.bind(this, 'team_users', team.id)}
+ className={'nav__sub-menu-item ' + this.isSelected('team_users', team.id)}
+ >
+ {team.name}
+ <span
+ className='menu-icon--right menu__close'
+ onClick={this.removeTeam.bind(this, team.id)}
+ style={{cursor: 'pointer'}}
+ >
+ {'x'}
+ </span>
+ </a>
+ </li>
+ <li>
+ <ul className='nav nav__inner-menu'>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('team_users', team.id)}
+ onClick={this.handleClick.bind(this, 'team_users', team.id)}
+ >
+ {'- Users'}
+ </a>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ );
+ }
+ }
+ }
+ }
+
return (
<div className='sidebar--left sidebar--collapsable'>
<div>
@@ -39,10 +133,18 @@ export default class AdminSidebar extends React.Component {
<li>
<ul className='nav nav__sub-menu'>
<li>
+ <h4>
+ <span className='icon fa fa-gear'></span>
+ <span>{'SETTINGS'}</span>
+ </h4>
+ </li>
+ </ul>
+ <ul className='nav nav__sub-menu padded'>
+ <li>
<a
href='#'
className={this.isSelected('service_settings')}
- onClick={this.handleClick.bind(this, 'service_settings')}
+ onClick={this.handleClick.bind(this, 'service_settings', null)}
>
{'Service Settings'}
</a>
@@ -51,7 +153,7 @@ export default class AdminSidebar extends React.Component {
<a
href='#'
className={this.isSelected('team_settings')}
- onClick={this.handleClick.bind(this, 'team_settings')}
+ onClick={this.handleClick.bind(this, 'team_settings', null)}
>
{'Team Settings'}
</a>
@@ -60,7 +162,7 @@ export default class AdminSidebar extends React.Component {
<a
href='#'
className={this.isSelected('sql_settings')}
- onClick={this.handleClick.bind(this, 'sql_settings')}
+ onClick={this.handleClick.bind(this, 'sql_settings', null)}
>
{'SQL Settings'}
</a>
@@ -69,7 +171,7 @@ export default class AdminSidebar extends React.Component {
<a
href='#'
className={this.isSelected('email_settings')}
- onClick={this.handleClick.bind(this, 'email_settings')}
+ onClick={this.handleClick.bind(this, 'email_settings', null)}
>
{'Email Settings'}
</a>
@@ -78,7 +180,7 @@ export default class AdminSidebar extends React.Component {
<a
href='#'
className={this.isSelected('image_settings')}
- onClick={this.handleClick.bind(this, 'image_settings')}
+ onClick={this.handleClick.bind(this, 'image_settings', null)}
>
{'File Settings'}
</a>
@@ -87,7 +189,7 @@ export default class AdminSidebar extends React.Component {
<a
href='#'
className={this.isSelected('log_settings')}
- onClick={this.handleClick.bind(this, 'log_settings')}
+ onClick={this.handleClick.bind(this, 'log_settings', null)}
>
{'Log Settings'}
</a>
@@ -95,17 +197,8 @@ export default class AdminSidebar extends React.Component {
<li>
<a
href='#'
- className={this.isSelected('logs')}
- onClick={this.handleClick.bind(this, 'logs')}
- >
- {'Logs'}
- </a>
- </li>
- <li>
- <a
- href='#'
className={this.isSelected('rate_settings')}
- onClick={this.handleClick.bind(this, 'rate_settings')}
+ onClick={this.handleClick.bind(this, 'rate_settings', null)}
>
{'Rate Limit Settings'}
</a>
@@ -114,7 +207,7 @@ export default class AdminSidebar extends React.Component {
<a
href='#'
className={this.isSelected('privacy_settings')}
- onClick={this.handleClick.bind(this, 'privacy_settings')}
+ onClick={this.handleClick.bind(this, 'privacy_settings', null)}
>
{'Privacy Settings'}
</a>
@@ -123,21 +216,73 @@ export default class AdminSidebar extends React.Component {
<a
href='#'
className={this.isSelected('gitlab_settings')}
- onClick={this.handleClick.bind(this, 'gitlab_settings')}
+ onClick={this.handleClick.bind(this, 'gitlab_settings', null)}
>
{'GitLab Settings'}
</a>
</li>
</ul>
+ <ul className='nav nav__sub-menu'>
+ <li>
+ <h4>
+ <span className='icon fa fa-gear'></span>
+ <span>{'TEAMS (' + count + ')'}</span>
+ <span className='menu-icon--right'>
+ <a
+ href='#'
+ onClick={this.showTeamSelect}
+ >
+ <i className='fa fa-plus'></i>
+ </a>
+ </span>
+ </h4>
+ </li>
+ </ul>
+ <ul className='nav nav__sub-menu padded'>
+ <li>
+ {teams}
+ </li>
+ </ul>
+ <ul className='nav nav__sub-menu'>
+ <li>
+ <h4>
+ <span className='icon fa fa-gear'></span>
+ <span>{'OTHER'}</span>
+ </h4>
+ </li>
+ </ul>
+ <ul className='nav nav__sub-menu padded'>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('logs')}
+ onClick={this.handleClick.bind(this, 'logs', null)}
+ >
+ {'Logs'}
+ </a>
+ </li>
+ </ul>
</li>
</ul>
</div>
+
+ <SelectTeamModal
+ teams={this.props.teams}
+ show={this.state.showSelectModal}
+ onModalSubmit={this.teamSelectedModal}
+ onModalDismissed={this.teamSelectedModalDismissed}
+ />
</div>
);
}
}
AdminSidebar.propTypes = {
+ teams: React.PropTypes.object,
+ selectedTeams: React.PropTypes.object,
+ removeSelectedTeam: React.PropTypes.func,
+ addSelectedTeam: React.PropTypes.func,
selected: React.PropTypes.string,
+ selectedTeam: React.PropTypes.string,
selectTab: React.PropTypes.func
}; \ No newline at end of file
diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx
index 854988947..3b5ad2a1a 100644
--- a/web/react/components/admin_console/email_settings.jsx
+++ b/web/react/components/admin_console/email_settings.jsx
@@ -211,7 +211,7 @@ export default class EmailSettings extends React.Component {
/>
{'false'}
</label>
- <p className='help-text'>{'Typically set to true in production. When true Mattermost will allow team creation and account signup utilizing email and password. You would set this to false if you only wanted to allow signup from a service like OAuth or LDAP.'}</p>
+ <p className='help-text'>{'When true, Mattermost allows team creation and account signup using email and password. This value should be false only when you want to limit signup to a single-sign-on service like OAuth or LDAP.'}</p>
</div>
</div>
@@ -244,7 +244,7 @@ export default class EmailSettings extends React.Component {
/>
{'false'}
</label>
- <p className='help-text'>{'Typically set to true in production. When true Mattermost will attempt to send email notifications. Developers may set this field to false skipping sending emails for faster development.'}</p>
+ <p className='help-text'>{'Typically set to true in production. When true, Mattermost attempts to send email notifications. Developers may set this field to false to skip email setup for faster development.'}</p>
</div>
</div>
@@ -279,7 +279,7 @@ export default class EmailSettings extends React.Component {
/>
{'false'}
</label>
- <p className='help-text'>{'Typically set to true in production. When true Mattermost will not allow a user to login without first having recieved an email with a verification link. Developers may set this field to false so skip sending verification emails for faster development.'}</p>
+ <p className='help-text'>{'Typically set to true in production. When true, Mattermost requires email verification after account creation prior to allowing login. Developers may set this field to false so skip sending verification emails for faster development.'}</p>
</div>
</div>
@@ -296,12 +296,12 @@ export default class EmailSettings extends React.Component {
className='form-control'
id='feedbackName'
ref='feedbackName'
- placeholder='Ex: "Mattermost", "System", "John Smith"'
+ placeholder='Ex: "Mattermost Notification", "System", "No-Reply"'
defaultValue={this.props.config.EmailSettings.FeedbackName}
onChange={this.handleChange}
disabled={!this.state.sendEmailNotifications}
/>
- <p className='help-text'>{'Name displayed on email account used when sending notification emails from Mattermost.'}</p>
+ <p className='help-text'>{'Display name on email account used when sending notification emails from Mattermost.'}</p>
</div>
</div>
@@ -323,7 +323,7 @@ export default class EmailSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.sendEmailNotifications}
/>
- <p className='help-text'>{'Email displayed on email account used when sending notification emails from Mattermost.'}</p>
+ <p className='help-text'>{'Email address displayed on email account used when sending notification emails from Mattermost.'}</p>
</div>
</div>
@@ -479,7 +479,7 @@ export default class EmailSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.sendEmailNotifications}
/>
- <p className='help-text'>{'32-character salt added to signing of email invites.'}</p>
+ <p className='help-text'>{'32-character salt added to signing of email invites. Randomly generated on install. Click "Re-Generate" to create new salt.'}</p>
<div className='help-text'>
<button
className='help-link'
@@ -510,7 +510,7 @@ export default class EmailSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.sendEmailNotifications}
/>
- <p className='help-text'>{'32-character salt added to signing of password reset emails.'}</p>
+ <p className='help-text'>{'32-character salt added to signing of password reset emails. Randomly generated on install. Click "Re-Generate" to create new salt.'}</p>
<div className='help-text'>
<button
className='help-link'
@@ -547,4 +547,4 @@ export default class EmailSettings extends React.Component {
EmailSettings.propTypes = {
config: React.PropTypes.object
-}; \ No newline at end of file
+};
diff --git a/web/react/components/admin_console/gitlab_settings.jsx b/web/react/components/admin_console/gitlab_settings.jsx
index f76655b89..1e10c5592 100644
--- a/web/react/components/admin_console/gitlab_settings.jsx
+++ b/web/react/components/admin_console/gitlab_settings.jsx
@@ -114,7 +114,7 @@ export default class GitLabSettings extends React.Component {
/>
{'false'}
</label>
- <p className='help-text'>{'When true Mattermost will allow team creation and account signup utilizing GitLab OAuth.'}</p>
+ <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>
</div>
</div>
@@ -136,7 +136,7 @@ export default class GitLabSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.Allow}
/>
- <p className='help-text'>{'Need help text.'}</p>
+ <p className='help-text'>{'Obtain this value via the instructions above for logging into GitLab.'}</p>
</div>
</div>
@@ -158,7 +158,7 @@ export default class GitLabSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.Allow}
/>
- <p className='help-text'>{'Need help text.'}</p>
+ <p className='help-text'>{'Obtain this value via the instructions above for logging into GitLab'}</p>
</div>
</div>
@@ -175,12 +175,12 @@ export default class GitLabSettings extends React.Component {
className='form-control'
id='Scope'
ref='Scope'
- placeholder='Ex ""'
+ placeholder='Not currently used by GitLab. Please leave blank'
defaultValue={this.props.config.GitLabSettings.Scope}
onChange={this.handleChange}
disabled={!this.state.Allow}
/>
- <p className='help-text'>{'Need help text.'}</p>
+ <p className='help-text'>{'This field is not yet used by GitLab OAuth. Other OAuth providers may use this field to specify the scope of account data from OAuth provider that is sent to Mattermost.'}</p>
</div>
</div>
@@ -202,7 +202,7 @@ export default class GitLabSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.Allow}
/>
- <p className='help-text'>{'Need help text.'}</p>
+ <p className='help-text'>{'Enter <your-gitlab-url>/oauth/authorize (example http://localhost:3000/oauth/authorize).'}</p>
</div>
</div>
@@ -224,7 +224,7 @@ export default class GitLabSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.Allow}
/>
- <p className='help-text'>{'Need help text.'}</p>
+ <p className='help-text'>{'Enter <your-gitlab-url>/oauth/token.'}</p>
</div>
</div>
@@ -246,7 +246,7 @@ export default class GitLabSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.Allow}
/>
- <p className='help-text'>{'Need help text.'}</p>
+ <p className='help-text'>{'Enter <your-gitlab-url>/api/v3/user.'}</p>
</div>
</div>
diff --git a/web/react/components/admin_console/image_settings.jsx b/web/react/components/admin_console/image_settings.jsx
index 25d5ad857..e52f516e8 100644
--- a/web/react/components/admin_console/image_settings.jsx
+++ b/web/react/components/admin_console/image_settings.jsx
@@ -457,7 +457,7 @@ export default class FileSettings extends React.Component {
defaultValue={this.props.config.FileSettings.PublicLinkSalt}
onChange={this.handleChange}
/>
- <p className='help-text'>{'32-character salt added to signing of public image links.'}</p>
+ <p className='help-text'>{'32-character salt added to signing of public image links. Randomly generated on install. Click "Re-Generate" to create new salt.'}</p>
<div className='help-text'>
<button
className='help-link'
diff --git a/web/react/components/admin_console/log_settings.jsx b/web/react/components/admin_console/log_settings.jsx
index d66801431..1c39c60e8 100644
--- a/web/react/components/admin_console/log_settings.jsx
+++ b/web/react/components/admin_console/log_settings.jsx
@@ -101,7 +101,7 @@ export default class LogSettings extends React.Component {
className='control-label col-sm-4'
htmlFor='consoleEnable'
>
- {'Log To the Console: '}
+ {'Log To The Console: '}
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
@@ -125,7 +125,7 @@ export default class LogSettings extends React.Component {
/>
{'false'}
</label>
- <p className='help-text'>{'Typically set to false in production. Developers may set this field to true to output log messages to console based on the console level option. If true then the server will output messages to the standard output stream (stdout).'}</p>
+ <p className='help-text'>{'Typically set to false in production. Developers may set this field to true to output log messages to console based on the console level option. If true, server writes messages to the standard output stream (stdout).'}</p>
</div>
</div>
@@ -149,7 +149,7 @@ export default class LogSettings extends React.Component {
<option value='INFO'>{'INFO'}</option>
<option value='ERROR'>{'ERROR'}</option>
</select>
- <p className='help-text'>{'This setting determines the level of detail at which log events are written to the console. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers debugging issues working on debugging issues.'}</p>
+ <p className='help-text'>{'This setting determines the level of detail at which log events are written to the console. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers working on debugging issues.'}</p>
</div>
</div>
@@ -181,7 +181,7 @@ export default class LogSettings extends React.Component {
/>
{'false'}
</label>
- <p className='help-text'>{'Typically set to true in production. When true log files are written to the file specified in file location field below.'}</p>
+ <p className='help-text'>{'Typically set to true in production. When true, log files are written to the log file specified in file location field below.'}</p>
</div>
</div>
@@ -205,7 +205,7 @@ export default class LogSettings extends React.Component {
<option value='INFO'>{'INFO'}</option>
<option value='ERROR'>{'ERROR'}</option>
</select>
- <p className='help-text'>{'This setting determines the level of detail at which log events are written to the file. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers debugging issues working on debugging issues.'}</p>
+ <p className='help-text'>{'This setting determines the level of detail at which log events are written to the log file. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers working on debugging issues.'}</p>
</div>
</div>
@@ -227,7 +227,7 @@ export default class LogSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.fileEnable}
/>
- <p className='help-text'>{'File to which log files are written. If blank, will be set to ./logs/mattermost.log. Log rotation is enabled and new files may be created in the same directory.'}</p>
+ <p className='help-text'>{'File to which log files are written. If blank, will be set to ./logs/mattermost, which writes logs to 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.'}</p>
</div>
</div>
diff --git a/web/react/components/admin_console/privacy_settings.jsx b/web/react/components/admin_console/privacy_settings.jsx
index 8ce693925..affd8ae11 100644
--- a/web/react/components/admin_console/privacy_settings.jsx
+++ b/web/react/components/admin_console/privacy_settings.jsx
@@ -99,7 +99,7 @@ export default class PrivacySettings extends React.Component {
/>
{'false'}
</label>
- <p className='help-text'>{'Hides email address of users from other users including team administrator.'}</p>
+ <p className='help-text'>{'When false, hides email address of users from other users in the user interface, including team owners and team administrators. Used when system is set up for managing teams where some users choose to keep their contact information private.'}</p>
</div>
</div>
@@ -132,7 +132,7 @@ export default class PrivacySettings extends React.Component {
/>
{'false'}
</label>
- <p className='help-text'>{'Hides full name of users from other users including team administrator.'}</p>
+ <p className='help-text'>{'When false, hides full name of users from other users including team owner and team administrators.'}</p>
</div>
</div>
diff --git a/web/react/components/admin_console/rate_settings.jsx b/web/react/components/admin_console/rate_settings.jsx
index c05bf4a82..0081daca3 100644
--- a/web/react/components/admin_console/rate_settings.jsx
+++ b/web/react/components/admin_console/rate_settings.jsx
@@ -140,7 +140,7 @@ export default class RateSettings extends React.Component {
/>
{'false'}
</label>
- <p className='help-text'>{'When enabled throttles rate at which APIs respond.'}</p>
+ <p className='help-text'>{'When true, APIs are throttled at rates specified below.'}</p>
</div>
</div>
@@ -184,7 +184,7 @@ export default class RateSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.EnableRateLimiter}
/>
- <p className='help-text'>{'Maximum number of users sessions connected to the system as determined by VaryByRemoteAddr and VaryByHeader variables.'}</p>
+ <p className='help-text'>{'Maximum number of users sessions connected to the system as determined by "Vary By Remote Address" and "Vary By Header" settings below.'}</p>
</div>
</div>
@@ -193,7 +193,7 @@ export default class RateSettings extends React.Component {
className='control-label col-sm-4'
htmlFor='VaryByRemoteAddr'
>
- {'Limit By Remote Address: '}
+ {'Vary By Remote Address: '}
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
@@ -219,7 +219,7 @@ export default class RateSettings extends React.Component {
/>
{'false'}
</label>
- <p className='help-text'>{'Rate limit API access by IP address.'}</p>
+ <p className='help-text'>{'When true, rate limit API access by IP address.'}</p>
</div>
</div>
@@ -228,7 +228,7 @@ export default class RateSettings extends React.Component {
className='control-label col-sm-4'
htmlFor='VaryByHeader'
>
- {'Limit By Http Header:'}
+ {'Vary By HTTP Header:'}
</label>
<div className='col-sm-8'>
<input
@@ -241,7 +241,7 @@ export default class RateSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.EnableRateLimiter || this.state.VaryByRemoteAddr}
/>
- <p className='help-text'>{'When filled in, 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").'}</p>
+ <p className='help-text'>{'When filled in, 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").'}</p>
</div>
</div>
diff --git a/web/react/components/admin_console/reset_password_modal.jsx b/web/react/components/admin_console/reset_password_modal.jsx
new file mode 100644
index 000000000..0b83edb17
--- /dev/null
+++ b/web/react/components/admin_console/reset_password_modal.jsx
@@ -0,0 +1,132 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var Modal = ReactBootstrap.Modal;
+
+export default class ResetPasswordModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.doSubmit = this.doSubmit.bind(this);
+ this.doCancel = this.doCancel.bind(this);
+
+ this.state = {
+ serverError: null
+ };
+ }
+
+ doSubmit(e) {
+ e.preventDefault();
+ var password = React.findDOMNode(this.refs.password).value;
+
+ if (!password || password.length < 5) {
+ this.setState({serverError: 'Please enter at least 5 characters.'});
+ return;
+ }
+
+ this.setState({serverError: null});
+
+ var data = {};
+ data.new_password = password;
+ data.name = this.props.team.name;
+ data.user_id = this.props.user.id;
+
+ Client.resetPassword(data,
+ () => {
+ this.props.onModalSubmit(React.findDOMNode(this.refs.password).value);
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ doCancel() {
+ this.setState({serverError: null});
+ this.props.onModalDismissed();
+ }
+
+ render() {
+ if (this.props.user == null) {
+ return <div/>;
+ }
+
+ let urlClass = 'input-group input-group--limit';
+ let serverError = null;
+
+ if (this.state.serverError) {
+ urlClass += ' has-error';
+ serverError = <div className='form-group has-error'><p className='input__help error'>{this.state.serverError}</p></div>;
+ }
+
+ return (
+ <Modal
+ show={this.props.show}
+ onHide={this.doCancel}
+ >
+ <Modal.Header closeButton={true}>
+ <Modal.Title>{'Reset Password'}</Modal.Title>
+ </Modal.Header>
+ <form
+ role='form'
+ className='form-horizontal'
+ >
+ <Modal.Body>
+ <div className='form-group'>
+ <div className='col-sm-10'>
+ <div className={urlClass}>
+ <span
+ data-toggle='tooltip'
+ title='New Password'
+ className='input-group-addon'
+ >
+ {'New Password'}
+ </span>
+ <input
+ type='password'
+ ref='password'
+ className='form-control'
+ maxLength='22'
+ autoFocus={true}
+ tabIndex='1'
+ />
+ </div>
+ {serverError}
+ </div>
+ </div>
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.doCancel}
+ >
+ {'Close'}
+ </button>
+ <button
+ onClick={this.doSubmit}
+ type='submit'
+ className='btn btn-primary'
+ tabIndex='2'
+ >
+ {'Select'}
+ </button>
+ </Modal.Footer>
+ </form>
+ </Modal>
+ );
+ }
+}
+
+ResetPasswordModal.defaultProps = {
+ show: false
+};
+
+ResetPasswordModal.propTypes = {
+ user: React.PropTypes.object,
+ team: React.PropTypes.object,
+ show: React.PropTypes.bool.isRequired,
+ onModalSubmit: React.PropTypes.func,
+ onModalDismissed: React.PropTypes.func
+};
diff --git a/web/react/components/admin_console/select_team_modal.jsx b/web/react/components/admin_console/select_team_modal.jsx
index fa30de7b2..343f65131 100644
--- a/web/react/components/admin_console/select_team_modal.jsx
+++ b/web/react/components/admin_console/select_team_modal.jsx
@@ -1,124 +1,99 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-export default class SelectTeam extends React.Component {
+var Modal = ReactBootstrap.Modal;
+
+export default class SelectTeamModal extends React.Component {
constructor(props) {
super(props);
- this.state = {
- };
+ this.doSubmit = this.doSubmit.bind(this);
+ this.doCancel = this.doCancel.bind(this);
}
+ doSubmit(e) {
+ e.preventDefault();
+ this.props.onModalSubmit(React.findDOMNode(this.refs.team).value);
+ }
+ doCancel() {
+ this.props.onModalDismissed();
+ }
render() {
+ if (this.props.teams == null) {
+ return <div/>;
+ }
+
+ var options = [];
+
+ for (var key in this.props.teams) {
+ if (this.props.teams.hasOwnProperty(key)) {
+ var team = this.props.teams[key];
+ options.push(
+ <option
+ key={'opt_' + team.id}
+ value={team.id}
+ >
+ {team.name}
+ </option>
+ );
+ }
+ }
+
return (
- <div className='modal fade'
- id='select-team'
- tabIndex='-1'
- role='dialog'
- aria-labelledby='teamsModalLabel'
+ <Modal
+ show={this.props.show}
+ onHide={this.doCancel}
>
- <div className='modal-dialog'
- role='document'
+ <Modal.Header closeButton={true}>
+ <Modal.Title>{'Select Team'}</Modal.Title>
+ </Modal.Header>
+ <form
+ role='form'
+ className='form-horizontal'
>
- <div className='modal-content'>
- <div className='modal-header'>
- <button
- type='button'
- className='close'
- data-dismiss='modal'
- aria-label='Close'
- >
- <span aria-hidden='true'>&times;</span>
- </button>
- <h4
- className='modal-title'
- id='teamsModalLabel'
- >
- {'Select a team'}
- </h4>
- </div>
- <div className='modal-body'>
- <table className='more-channel-table table'>
- <tbody>
- <tr>
- <td>
- <p className='more-channel-name'>{'Descartes'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Grouping'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Adventure'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Crossroads'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Sky scraping'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Outdoors'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Microsoft'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Apple'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <div className='modal-footer'>
- <button
- type='button'
- className='btn btn-default'
- data-dismiss='modal'
- >
- {'Close'}
- </button>
+ <Modal.Body>
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ <select
+ ref='team'
+ size='10'
+ style={{width: '100%'}}
+ >
+ {options}
+ </select>
+ </div>
</div>
- </div>
- </div>
- </div>
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.doCancel}
+ >
+ {'Close'}
+ </button>
+ <button
+ onClick={this.doSubmit}
+ type='submit'
+ className='btn btn-primary'
+ tabIndex='2'
+ >
+ {'Select'}
+ </button>
+ </Modal.Footer>
+ </form>
+ </Modal>
);
}
-} \ No newline at end of file
+}
+
+SelectTeamModal.defaultProps = {
+ show: false
+};
+
+SelectTeamModal.propTypes = {
+ teams: React.PropTypes.object,
+ show: React.PropTypes.bool.isRequired,
+ onModalSubmit: React.PropTypes.func,
+ onModalDismissed: React.PropTypes.func
+};
diff --git a/web/react/components/admin_console/service_settings.jsx b/web/react/components/admin_console/service_settings.jsx
index 1bb1f053b..245ffa871 100644
--- a/web/react/components/admin_console/service_settings.jsx
+++ b/web/react/components/admin_console/service_settings.jsx
@@ -35,7 +35,7 @@ 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.EnableOAuthServiceProvider = React.findDOMNode(this.refs.EnableOAuthServiceProvider).checked;
config.ServiceSettings.EnableIncomingWebhooks = React.findDOMNode(this.refs.EnableIncomingWebhooks).checked;
config.ServiceSettings.EnableTesting = React.findDOMNode(this.refs.EnableTesting).checked;
@@ -103,7 +103,7 @@ export default class ServiceSettings extends React.Component {
defaultValue={this.props.config.ServiceSettings.ListenAddress}
onChange={this.handleChange}
/>
- <p className='help-text'>{'The address to bind to and listen. ":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.'}</p>
+ <p className='help-text'>{'The address to which to bind and listen. 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.'}</p>
</div>
</div>
@@ -173,42 +173,9 @@ export default class ServiceSettings extends React.Component {
<div className='form-group'>
<label
className='control-label col-sm-4'
- htmlFor='EnableOAuthServiceProvider'
- >
- {'Enable OAuth Service Provider: '}
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableOAuthServiceProvider'
- value='true'
- ref='EnableOAuthServiceProvider'
- defaultChecked={this.props.config.ServiceSettings.EnableOAuthServiceProvider}
- onChange={this.handleChange}
- />
- {'true'}
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableOAuthServiceProvider'
- value='false'
- defaultChecked={!this.props.config.ServiceSettings.EnableOAuthServiceProvider}
- onChange={this.handleChange}
- />
- {'false'}
- </label>
- <p className='help-text'>{'When enabled Mattermost will act as an Oauth2 Provider. Changing this will require a server restart before taking effect.'}</p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
htmlFor='EnableIncomingWebhooks'
>
- {'EnableIncomingWebhooks: '}
+ {'Enable Incoming Webhooks: '}
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
@@ -232,7 +199,7 @@ export default class ServiceSettings extends React.Component {
/>
{'false'}
</label>
- <p className='help-text'>{'When true incomming web hooks will be allowed.'}</p>
+ <p className='help-text'>{'When true, incoming webhooks will be allowed.'}</p>
</div>
</div>
@@ -265,7 +232,7 @@ export default class ServiceSettings extends React.Component {
/>
{'false'}
</label>
- <p className='help-text'>{'When true slash commands like /loadtest are enabled in the add comment box. Changing this will require a server restart before taking effect. Typically used for development.'}</p>
+ <p className='help-text'>{'(Developer Option) When true, /loadtest slash command is enabled to load test accounts and test data. Changing this will require a server restart before taking effect.'}</p>
</div>
</div>
@@ -291,6 +258,39 @@ export default class ServiceSettings extends React.Component {
}
}
+// <div className='form-group'>
+// <label
+// className='control-label col-sm-4'
+// htmlFor='EnableOAuthServiceProvider'
+// >
+// {'Enable OAuth Service Provider: '}
+// </label>
+// <div className='col-sm-8'>
+// <label className='radio-inline'>
+// <input
+// type='radio'
+// name='EnableOAuthServiceProvider'
+// value='true'
+// ref='EnableOAuthServiceProvider'
+// defaultChecked={this.props.config.ServiceSettings.EnableOAuthServiceProvider}
+// onChange={this.handleChange}
+// />
+// {'true'}
+// </label>
+// <label className='radio-inline'>
+// <input
+// type='radio'
+// name='EnableOAuthServiceProvider'
+// value='false'
+// defaultChecked={!this.props.config.ServiceSettings.EnableOAuthServiceProvider}
+// onChange={this.handleChange}
+// />
+// {'false'}
+// </label>
+// <p className='help-text'>{'When enabled Mattermost will act as an OAuth2 Provider. Changing this will require a server restart before taking effect.'}</p>
+// </div>
+// </div>
+
ServiceSettings.propTypes = {
config: React.PropTypes.object
};
diff --git a/web/react/components/admin_console/sql_settings.jsx b/web/react/components/admin_console/sql_settings.jsx
index ad01b5963..430a7453b 100644
--- a/web/react/components/admin_console/sql_settings.jsx
+++ b/web/react/components/admin_console/sql_settings.jsx
@@ -252,7 +252,7 @@ export default class SqlSettings extends React.Component {
/>
{'false'}
</label>
- <p className='help-text'>{'Output executing SQL statements to the log. Typically used for development.'}</p>
+ <p className='help-text'>{'(Development Mode) When true, executing SQL statements are written to the log.'}</p>
</div>
</div>
diff --git a/web/react/components/admin_console/team_settings.jsx b/web/react/components/admin_console/team_settings.jsx
index fefc0e936..0f6f819d3 100644
--- a/web/react/components/admin_console/team_settings.jsx
+++ b/web/react/components/admin_console/team_settings.jsx
@@ -28,7 +28,6 @@ export default class TeamSettings extends React.Component {
var config = this.props.config;
config.TeamSettings.SiteName = React.findDOMNode(this.refs.SiteName).value.trim();
- config.TeamSettings.DefaultThemeColor = React.findDOMNode(this.refs.DefaultThemeColor).value.trim();
config.TeamSettings.RestrictCreationToDomains = React.findDOMNode(this.refs.RestrictCreationToDomains).value.trim();
config.TeamSettings.EnableTeamCreation = React.findDOMNode(this.refs.EnableTeamCreation).checked;
config.TeamSettings.EnableUserCreation = React.findDOMNode(this.refs.EnableUserCreation).checked;
@@ -118,28 +117,7 @@ export default class TeamSettings extends React.Component {
defaultValue={this.props.config.TeamSettings.MaxUsersPerTeam}
onChange={this.handleChange}
/>
- <p className='help-text'>{'Maximum number of users per team.'}</p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='DefaultThemeColor'
- >
- {'Default Theme Color:'}
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='DefaultThemeColor'
- ref='DefaultThemeColor'
- placeholder='Ex "#2389D7"'
- defaultValue={this.props.config.TeamSettings.DefaultThemeColor}
- onChange={this.handleChange}
- />
- <p className='help-text'>{'Default theme color for team sites.'}</p>
+ <p className='help-text'>{'Maximum total number of users per team, including both active and inactive users.'}</p>
</div>
</div>
@@ -172,7 +150,7 @@ export default class TeamSettings extends React.Component {
/>
{'false'}
</label>
- <p className='help-text'>{'When false the ability to create teams is disabled. The create team button displays error when pressed.'}</p>
+ <p className='help-text'>{'When false, the ability to create teams is disabled. The create team button displays error when pressed.'}</p>
</div>
</div>
@@ -205,7 +183,7 @@ export default class TeamSettings extends React.Component {
/>
{'false'}
</label>
- <p className='help-text'>{'When false the ability to create accounts is disabled. The create account button displays error when pressed.'}</p>
+ <p className='help-text'>{'When false, the ability to create accounts is disabled. The create account button displays error when pressed.'}</p>
</div>
</div>
diff --git a/web/react/components/admin_console/team_users.jsx b/web/react/components/admin_console/team_users.jsx
new file mode 100644
index 000000000..0a971ff15
--- /dev/null
+++ b/web/react/components/admin_console/team_users.jsx
@@ -0,0 +1,178 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var LoadingScreen = require('../loading_screen.jsx');
+var UserItem = require('./user_item.jsx');
+var ResetPasswordModal = require('./reset_password_modal.jsx');
+
+export default class UserList extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getTeamProfiles = this.getTeamProfiles.bind(this);
+ this.getCurrentTeamProfiles = this.getCurrentTeamProfiles.bind(this);
+ this.doPasswordReset = this.doPasswordReset.bind(this);
+ this.doPasswordResetDismiss = this.doPasswordResetDismiss.bind(this);
+ this.doPasswordResetSubmit = this.doPasswordResetSubmit.bind(this);
+
+ this.state = {
+ teamId: props.team.id,
+ users: null,
+ serverError: null,
+ showPasswordModal: false,
+ user: null
+ };
+ }
+
+ componentDidMount() {
+ this.getCurrentTeamProfiles();
+ }
+
+ getCurrentTeamProfiles() {
+ this.getTeamProfiles(this.props.team.id);
+ }
+
+ // this.setState({
+ // teamId: this.state.teamId,
+ // users: this.state.users,
+ // serverError: this.state.serverError,
+ // showPasswordModal: this.state.showPasswordModal,
+ // user: this.state.user
+ // });
+
+ getTeamProfiles(teamId) {
+ Client.getProfilesForTeam(
+ teamId,
+ (users) => {
+ var memberList = [];
+ for (var id in users) {
+ if (users.hasOwnProperty(id)) {
+ memberList.push(users[id]);
+ }
+ }
+
+ memberList.sort((a, b) => {
+ if (a.username < b.username) {
+ return -1;
+ }
+
+ if (a.username > b.username) {
+ return 1;
+ }
+
+ return 0;
+ });
+
+ this.setState({
+ teamId: this.state.teamId,
+ users: memberList,
+ serverError: this.state.serverError,
+ showPasswordModal: this.state.showPasswordModal,
+ user: this.state.user
+ });
+ },
+ (err) => {
+ this.setState({
+ teamId: this.state.teamId,
+ users: null,
+ serverError: err.message,
+ showPasswordModal: this.state.showPasswordModal,
+ user: this.state.user
+ });
+ }
+ );
+ }
+
+ doPasswordReset(user) {
+ this.setState({
+ teamId: this.state.teamId,
+ users: this.state.users,
+ serverError: this.state.serverError,
+ showPasswordModal: true,
+ user
+ });
+ }
+
+ doPasswordResetDismiss() {
+ this.state.showPasswordModal = false;
+ this.state.user = null;
+ this.setState({
+ teamId: this.state.teamId,
+ users: this.state.users,
+ serverError: this.state.serverError,
+ showPasswordModal: false,
+ user: null
+ });
+ }
+
+ doPasswordResetSubmit() {
+ this.setState({
+ teamId: this.state.teamId,
+ users: this.state.users,
+ serverError: this.state.serverError,
+ showPasswordModal: false,
+ user: null
+ });
+ }
+
+ componentWillReceiveProps(newProps) {
+ this.getTeamProfiles(newProps.team.id);
+ }
+
+ componentWillUnmount() {
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ if (this.state.users == null) {
+ return (
+ <div className='wrapper--fixed'>
+ <h3>{'Users for ' + this.props.team.name}</h3>
+ {serverError}
+ <LoadingScreen />
+ </div>
+ );
+ }
+
+ var memberList = this.state.users.map((user) => {
+ return (
+ <UserItem
+ key={'user_' + user.id}
+ user={user}
+ refreshProfiles={this.getCurrentTeamProfiles}
+ doPasswordReset={this.doPasswordReset}
+ />);
+ });
+
+ return (
+ <div className='wrapper--fixed'>
+ <h3>{'Users for ' + this.props.team.name + ' (' + this.state.users.length + ')'}</h3>
+ {serverError}
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+ <div className='member-list-holder'>
+ {memberList}
+ </div>
+ </form>
+ <ResetPasswordModal
+ user={this.state.user}
+ show={this.state.showPasswordModal}
+ team={this.props.team}
+ onModalSubmit={this.doPasswordResetSubmit}
+ onModalDismissed={this.doPasswordResetDismiss}
+ />
+ </div>
+ );
+ }
+}
+
+UserList.propTypes = {
+ team: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/user_item.jsx b/web/react/components/admin_console/user_item.jsx
new file mode 100644
index 000000000..32812e875
--- /dev/null
+++ b/web/react/components/admin_console/user_item.jsx
@@ -0,0 +1,266 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var Utils = require('../../utils/utils.jsx');
+
+export default class UserItem extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleMakeMember = this.handleMakeMember.bind(this);
+ this.handleMakeActive = this.handleMakeActive.bind(this);
+ this.handleMakeNotActive = this.handleMakeNotActive.bind(this);
+ this.handleMakeAdmin = this.handleMakeAdmin.bind(this);
+ this.handleMakeSystemAdmin = this.handleMakeSystemAdmin.bind(this);
+ this.handleResetPassword = this.handleResetPassword.bind(this);
+
+ this.state = {};
+ }
+
+ handleMakeMember(e) {
+ e.preventDefault();
+ const data = {
+ user_id: this.props.user.id,
+ new_roles: ''
+ };
+
+ Client.updateRoles(data,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeActive(e) {
+ e.preventDefault();
+ Client.updateActive(this.props.user.id, true,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeNotActive(e) {
+ e.preventDefault();
+ Client.updateActive(this.props.user.id, false,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeAdmin(e) {
+ e.preventDefault();
+ const data = {
+ user_id: this.props.user.id,
+ new_roles: 'admin'
+ };
+
+ Client.updateRoles(data,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeSystemAdmin(e) {
+ e.preventDefault();
+ const data = {
+ user_id: this.props.user.id,
+ new_roles: 'system_admin'
+ };
+
+ Client.updateRoles(data,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleResetPassword(e) {
+ e.preventDefault();
+ this.props.doPasswordReset(this.props.user);
+ }
+
+ render() {
+ let serverError = null;
+ if (this.state.serverError) {
+ serverError = (
+ <div className='has-error'>
+ <label className='has-error control-label'>{this.state.serverError}</label>
+ </div>
+ );
+ }
+
+ const user = this.props.user;
+ let currentRoles = 'Member';
+ if (user.roles.length > 0) {
+ if (user.roles.indexOf('system_admin') > -1) {
+ currentRoles = 'System Admin';
+ } else {
+ currentRoles = user.roles.charAt(0).toUpperCase() + user.roles.slice(1);
+ }
+ }
+
+ const email = user.email;
+ let showMakeMember = user.roles === 'admin' || user.roles === 'system_admin';
+ let showMakeAdmin = user.roles === '' || user.roles === 'system_admin';
+ let showMakeSystemAdmin = user.roles === '' || user.roles === 'admin';
+ let showMakeActive = false;
+ let showMakeNotActive = user.roles !== 'system_admin';
+
+ if (user.delete_at > 0) {
+ currentRoles = 'Inactive';
+ currentRoles = 'Inactive';
+ showMakeMember = false;
+ showMakeAdmin = false;
+ showMakeSystemAdmin = false;
+ showMakeActive = true;
+ showMakeNotActive = false;
+ }
+
+ let makeSystemAdmin = null;
+ if (showMakeSystemAdmin) {
+ makeSystemAdmin = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeSystemAdmin}
+ >
+ {'Make System Admin'}
+ </a>
+ </li>
+ );
+ }
+
+ let makeAdmin = null;
+ if (showMakeAdmin) {
+ makeAdmin = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeAdmin}
+ >
+ {'Make Admin'}
+ </a>
+ </li>
+ );
+ }
+
+ let makeMember = null;
+ if (showMakeMember) {
+ makeMember = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeMember}
+ >
+ {'Make Member'}
+ </a>
+ </li>
+ );
+ }
+
+ let makeActive = null;
+ if (showMakeActive) {
+ makeActive = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeActive}
+ >
+ {'Make Active'}
+ </a>
+ </li>
+ );
+ }
+
+ let makeNotActive = null;
+ if (showMakeNotActive) {
+ makeNotActive = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeNotActive}
+ >
+ {'Make Inactive'}
+ </a>
+ </li>
+ );
+ }
+
+ return (
+ <div className='row member-div'>
+ <img
+ className='post-profile-img pull-left'
+ src={`/api/v1/users/${user.id}/image?time=${user.update_at}`}
+ height='36'
+ width='36'
+ />
+ <span className='member-name'>{Utils.getDisplayName(user)}</span>
+ <span className='member-email'>{email}</span>
+ <div className='dropdown member-drop'>
+ <a
+ href='#'
+ className='dropdown-toggle theme'
+ type='button'
+ id='channel_header_dropdown'
+ data-toggle='dropdown'
+ aria-expanded='true'
+ >
+ <span>{currentRoles} </span>
+ <span className='caret'></span>
+ </a>
+ <ul
+ className='dropdown-menu member-menu'
+ role='menu'
+ aria-labelledby='channel_header_dropdown'
+ >
+ {makeAdmin}
+ {makeMember}
+ {makeActive}
+ {makeNotActive}
+ {makeSystemAdmin}
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleResetPassword}
+ >
+ {'Reset Password'}
+ </a>
+ </li>
+ </ul>
+ </div>
+ {serverError}
+ </div>
+ );
+ }
+}
+
+UserItem.propTypes = {
+ user: React.PropTypes.object.isRequired,
+ refreshProfiles: React.PropTypes.func.isRequired,
+ doPasswordReset: React.PropTypes.func.isRequired
+};
diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx
index 962ba26ee..39c86405c 100644
--- a/web/react/components/channel_loader.jsx
+++ b/web/react/components/channel_loader.jsx
@@ -109,6 +109,13 @@ export default class ChannelLoader extends React.Component {
$('.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')) {
+ e.preventDefault();
+ }
+ });
}
componentWillUnmount() {
clearInterval(this.intervalId);
@@ -123,6 +130,8 @@ export default class ChannelLoader extends React.Component {
$('body').off('mouseenter mouseleave', '.post.post--comment.same--root');
$('.modal').off('show.bs.modal');
+
+ $(window).off('keydown.preventBackspace');
}
onSocketChange(msg) {
if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) {
diff --git a/web/react/components/channel_notifications.jsx b/web/react/components/channel_notifications.jsx
index 83067240d..9eda68b38 100644
--- a/web/react/components/channel_notifications.jsx
+++ b/web/react/components/channel_notifications.jsx
@@ -163,10 +163,22 @@ export default class ChannelNotifications extends React.Component {
}.bind(this);
let curChannel = ChannelStore.get(this.state.channelId);
- let extraInfo = (<span>These settings will override the global notification settings</span>);
+ let extraInfo = (
+ <span>
+ These settings will override the global notification settings.
+ <br/>
+ Desktop notifications are available on Firefox, Safari, and Chrome.
+ </span>
+ );
if (curChannel && curChannel.display_name) {
- extraInfo = (<span>These settings will override the global notification settings for the <b>{curChannel.display_name}</b> channel</span>);
+ extraInfo = (
+ <span>
+ These settings will override the global notification settings for the <b>{curChannel.display_name}</b> channel.
+ <br/>
+ Desktop notifications are available on Firefox, Safari, and Chrome.
+ </span>
+ );
}
return (
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index c2fc0dcf3..5097b3aa5 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -28,6 +28,7 @@ export default class CreateComment extends React.Component {
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
+ this.handleTextDrop = this.handleTextDrop.bind(this);
this.removePreview = this.removePreview.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.getFileCount = this.getFileCount.bind(this);
@@ -42,6 +43,12 @@ export default class CreateComment extends React.Component {
submitting: false
};
}
+ componentDidUpdate(prevProps, prevState) {
+ if (prevState.uploadsInProgress < this.state.uploadsInProgress) {
+ $('.post-right__scroll').scrollTop($('.post-right__scroll')[0].scrollHeight);
+ $('.post-right__scroll').perfectScrollbar('update');
+ }
+ }
handleSubmit(e) {
e.preventDefault();
@@ -178,6 +185,11 @@ export default class CreateComment extends React.Component {
this.setState({serverError: err});
}
}
+ handleTextDrop(text) {
+ const newText = this.state.messageText + text;
+ this.handleUserInput(newText);
+ Utils.setCaretPosition(React.findDOMNode(this.refs.textbox.refs.message), newText.length);
+ }
removePreview(id) {
let previews = this.state.previews;
let uploadsInProgress = this.state.uploadsInProgress;
@@ -264,6 +276,7 @@ export default class CreateComment extends React.Component {
onUploadStart={this.handleUploadStart}
onFileUpload={this.handleFileUploadComplete}
onUploadError={this.handleUploadError}
+ onTextDrop={this.handleTextDrop}
postType='comment'
channelId={this.props.channelId}
/>
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index abad60154..0cd14747d 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -31,6 +31,7 @@ export default class CreatePost extends React.Component {
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
+ this.handleTextDrop = this.handleTextDrop.bind(this);
this.removePreview = this.removePreview.bind(this);
this.onChange = this.onChange.bind(this);
this.getFileCount = this.getFileCount.bind(this);
@@ -51,6 +52,12 @@ export default class CreatePost extends React.Component {
componentDidUpdate(prevProps, prevState) {
if (prevState.previews.length !== this.state.previews.length) {
this.resizePostHolder();
+ return;
+ }
+
+ if (prevState.uploadsInProgress !== this.state.uploadsInProgress) {
+ this.resizePostHolder();
+ return;
}
}
getCurrentDraft() {
@@ -78,7 +85,7 @@ export default class CreatePost extends React.Component {
return;
}
- let post = {};
+ const post = {};
post.filenames = [];
post.message = this.state.messageText;
@@ -98,20 +105,20 @@ export default class CreatePost extends React.Component {
this.state.channelId,
post.message,
false,
- function handleCommandSuccess(data) {
+ (data) => {
PostStore.storeDraft(data.channel_id, null);
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
if (data.goto_location.length > 0) {
window.location.href = data.goto_location;
}
- }.bind(this),
- function handleCommandError(err) {
- let state = {};
+ },
+ (err) => {
+ const state = {};
state.serverError = err.message;
state.submitting = false;
this.setState(state);
- }.bind(this)
+ }
);
} else {
post.channel_id = this.state.channelId;
@@ -132,10 +139,10 @@ export default class CreatePost extends React.Component {
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
Client.createPost(post, channel,
- function handlePostSuccess(data) {
+ (data) => {
AsyncClient.getPosts();
- let member = ChannelStore.getMember(channel.id);
+ const member = ChannelStore.getMember(channel.id);
member.msg_count = channel.total_msg_count;
member.last_viewed_at = Date.now();
ChannelStore.setChannelMember(member);
@@ -145,8 +152,8 @@ export default class CreatePost extends React.Component {
post: data
});
},
- function handlePostError(err) {
- let state = {};
+ (err) => {
+ const state = {};
if (err.message === 'Invalid RootId parameter') {
if ($('#post_deleted').length > 0) {
@@ -160,7 +167,7 @@ export default class CreatePost extends React.Component {
state.submitting = false;
this.setState(state);
- }.bind(this)
+ }
);
}
}
@@ -178,9 +185,9 @@ export default class CreatePost extends React.Component {
}
}
handleUserInput(messageText) {
- this.setState({messageText: messageText});
+ this.setState({messageText});
- let draft = PostStore.getCurrentDraft();
+ const draft = PostStore.getCurrentDraft();
draft.message = messageText;
PostStore.storeCurrentDraft(draft);
}
@@ -190,7 +197,7 @@ export default class CreatePost extends React.Component {
$(window).trigger('resize');
}
handleUploadStart(clientIds, channelId) {
- let draft = PostStore.getDraft(channelId);
+ const draft = PostStore.getDraft(channelId);
draft.uploadsInProgress = draft.uploadsInProgress.concat(clientIds);
PostStore.storeDraft(channelId, draft);
@@ -198,7 +205,7 @@ export default class CreatePost extends React.Component {
this.setState({uploadsInProgress: draft.uploadsInProgress});
}
handleFileUploadComplete(filenames, clientIds, channelId) {
- let draft = PostStore.getDraft(channelId);
+ const draft = PostStore.getDraft(channelId);
// remove each finished file from uploads
for (let i = 0; i < clientIds.length; i++) {
@@ -216,7 +223,7 @@ export default class CreatePost extends React.Component {
}
handleUploadError(err, clientId) {
if (clientId !== -1) {
- let draft = PostStore.getDraft(this.state.channelId);
+ const draft = PostStore.getDraft(this.state.channelId);
const index = draft.uploadsInProgress.indexOf(clientId);
if (index !== -1) {
@@ -230,9 +237,14 @@ export default class CreatePost extends React.Component {
this.setState({serverError: err});
}
}
+ handleTextDrop(text) {
+ const newText = this.state.messageText + text;
+ this.handleUserInput(newText);
+ Utils.setCaretPosition(React.findDOMNode(this.refs.textbox.refs.message), newText.length);
+ }
removePreview(id) {
- let previews = this.state.previews;
- let uploadsInProgress = this.state.uploadsInProgress;
+ const previews = Object.assign([], this.state.previews);
+ const uploadsInProgress = this.state.uploadsInProgress;
// id can either be the path of an uploaded file or the client id of an in progress upload
let index = previews.indexOf(id);
@@ -247,12 +259,12 @@ export default class CreatePost extends React.Component {
}
}
- let draft = PostStore.getCurrentDraft();
+ const draft = PostStore.getCurrentDraft();
draft.previews = previews;
draft.uploadsInProgress = uploadsInProgress;
PostStore.storeCurrentDraft(draft);
- this.setState({previews: previews, uploadsInProgress: uploadsInProgress});
+ this.setState({previews, uploadsInProgress});
}
componentDidMount() {
ChannelStore.addChangeListener(this.onChange);
@@ -266,7 +278,7 @@ export default class CreatePost extends React.Component {
if (this.state.channelId !== channelId) {
const draft = this.getCurrentDraft();
- this.setState({channelId: channelId, messageText: draft.messageText, initialText: draft.messageText, submitting: false, serverError: null, postError: null, previews: draft.previews, uploadsInProgress: draft.uploadsInProgress});
+ this.setState({channelId, messageText: draft.messageText, initialText: draft.messageText, submitting: false, serverError: null, postError: null, previews: draft.previews, uploadsInProgress: draft.uploadsInProgress});
}
}
getFileCount(channelId) {
@@ -334,6 +346,7 @@ export default class CreatePost extends React.Component {
onUploadStart={this.handleUploadStart}
onFileUpload={this.handleFileUploadComplete}
onUploadError={this.handleUploadError}
+ onTextDrop={this.handleTextDrop}
postType='post'
channelId=''
/>
diff --git a/web/react/components/delete_channel_modal.jsx b/web/react/components/delete_channel_modal.jsx
index 4efb9cb23..44c54db72 100644
--- a/web/react/components/delete_channel_modal.jsx
+++ b/web/react/components/delete_channel_modal.jsx
@@ -4,6 +4,7 @@
const Client = require('../utils/client.jsx');
const AsyncClient = require('../utils/async_client.jsx');
const ChannelStore = require('../stores/channel_store.jsx');
+var TeamStore = require('../stores/team_store.jsx');
export default class DeleteChannelModal extends React.Component {
constructor(props) {
@@ -24,7 +25,7 @@ export default class DeleteChannelModal extends React.Component {
Client.deleteChannel(this.state.channelId,
function handleDeleteSuccess() {
AsyncClient.getChannels(true);
- window.location.href = '/';
+ window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square';
},
function handleDeleteError(err) {
AsyncClient.dispatchError(err, 'handleDelete');
diff --git a/web/react/components/error_bar.jsx b/web/react/components/error_bar.jsx
index 87d94a41d..05726e860 100644
--- a/web/react/components/error_bar.jsx
+++ b/web/react/components/error_bar.jsx
@@ -2,10 +2,6 @@
// See License.txt for license information.
var ErrorStore = require('../stores/error_store.jsx');
-var utils = require('../utils/utils.jsx');
-var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-var Constants = require('../utils/constants.jsx');
-var ActionTypes = Constants.ActionTypes;
export default class ErrorBar extends React.Component {
constructor() {
@@ -13,70 +9,79 @@ export default class ErrorBar extends React.Component {
this.onErrorChange = this.onErrorChange.bind(this);
this.handleClose = this.handleClose.bind(this);
+ this.prevTimer = null;
- this.state = this.getStateFromStores();
- if (this.state.message) {
- setTimeout(this.handleClose, 10000);
+ this.state = ErrorStore.getLastError();
+ if (this.state && this.state.message) {
+ this.prevTimer = setTimeout(this.handleClose, 10000);
}
}
- getStateFromStores() {
- var error = ErrorStore.getLastError();
- if (!error || error.message === 'There appears to be a problem with your internet connection') {
- return {message: null};
- }
- return {message: error.message};
- }
componentDidMount() {
ErrorStore.addChangeListener(this.onErrorChange);
$('body').css('padding-top', $(React.findDOMNode(this)).outerHeight());
- $(window).resize(function onResize() {
- if (this.state.message) {
+ $(window).resize(() => {
+ if (this.state && this.state.message) {
$('body').css('padding-top', $(React.findDOMNode(this)).outerHeight());
}
- }.bind(this));
+ });
}
+
componentWillUnmount() {
ErrorStore.removeChangeListener(this.onErrorChange);
}
+
onErrorChange() {
- var newState = this.getStateFromStores();
- if (!utils.areStatesEqual(newState, this.state)) {
- if (newState.message) {
- setTimeout(this.handleClose, 10000);
- }
+ var newState = ErrorStore.getLastError();
+
+ if (this.prevTimer != null) {
+ clearInterval(this.prevTimer);
+ this.prevTimer = null;
+ }
+ if (newState) {
this.setState(newState);
+ this.prevTimer = setTimeout(this.handleClose, 10000);
+ } else {
+ this.setState({message: null});
}
}
+
handleClose(e) {
if (e) {
e.preventDefault();
}
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_ERROR,
- err: null
- });
+ ErrorStore.storeLastError(null);
+ ErrorStore.emitChange();
$('body').css('padding-top', '0');
}
+
render() {
- if (this.state.message) {
- return (
- <div className='error-bar'>
- <span>{this.state.message}</span>
- <a
- href='#'
- className='error-bar__close'
- onClick={this.handleClose}
- >
- &times;
- </a>
- </div>
- );
+ if (!this.state) {
+ return <div/>;
+ }
+
+ if (!this.state.message) {
+ return <div/>;
+ }
+
+ if (this.state.connErrorCount < 7) {
+ return <div/>;
}
- return <div/>;
+ return (
+ <div className='error-bar'>
+ <span>{this.state.message}</span>
+ <a
+ href='#'
+ className='error-bar__close'
+ onClick={this.handleClose}
+ >
+ &times;
+ </a>
+ </div>
+ );
}
}
diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx
index c9aa06a97..888f24aa5 100644
--- a/web/react/components/file_attachment.jsx
+++ b/web/react/components/file_attachment.jsx
@@ -143,10 +143,7 @@ export default class FileAttachment extends React.Component {
>
<a className='post-image__thumbnail'
href='#'
- onClick={this.props.handleImageClick}
- data-img-id={this.props.index}
- data-toggle='modal'
- data-target={'#' + this.props.modalId}
+ onClick={() => this.props.handleImageClick(this.props.index)}
>
{thumbnail}
</a>
@@ -187,9 +184,6 @@ FileAttachment.propTypes = {
// the index of this attachment preview in the parent FileAttachmentList
index: React.PropTypes.number.isRequired,
- // the identifier of the modal dialog used to preview files
- modalId: React.PropTypes.string.isRequired,
-
- // handler for when the thumbnail is clicked
+ // handler for when the thumbnail is clicked passed the index above
handleImageClick: React.PropTypes.func
};
diff --git a/web/react/components/file_attachment_list.jsx b/web/react/components/file_attachment_list.jsx
index abe72089a..212d4a958 100644
--- a/web/react/components/file_attachment_list.jsx
+++ b/web/react/components/file_attachment_list.jsx
@@ -11,23 +11,21 @@ export default class FileAttachmentList extends React.Component {
this.handleImageClick = this.handleImageClick.bind(this);
- this.state = {startImgId: 0};
+ this.state = {showPreviewModal: false, startImgId: 0};
}
- handleImageClick(e) {
- this.setState({startImgId: parseInt($(e.target.parentNode).attr('data-img-id'), 10)});
+ handleImageClick(indexClicked) {
+ this.setState({showPreviewModal: true, startImgId: indexClicked});
}
render() {
var filenames = this.props.filenames;
- var modalId = this.props.modalId;
var postFiles = [];
for (var i = 0; i < filenames.length && i < Constants.MAX_DISPLAY_FILES; i++) {
postFiles.push(
<FileAttachment
- key={i}
+ key={'file_attachment_' + i}
filename={filenames[i]}
index={i}
- modalId={modalId}
handleImageClick={this.handleImageClick}
/>
);
@@ -39,9 +37,10 @@ export default class FileAttachmentList extends React.Component {
{postFiles}
</div>
<ViewImageModal
+ show={this.state.showPreviewModal}
+ onModalDismissed={() => this.setState({showPreviewModal: false})}
channelId={this.props.channelId}
userId={this.props.userId}
- modalId={modalId}
startId={this.state.startImgId}
filenames={filenames}
/>
@@ -55,9 +54,6 @@ FileAttachmentList.propTypes = {
// a list of file pathes displayed by this
filenames: React.PropTypes.arrayOf(React.PropTypes.string).isRequired,
- // the identifier of the modal dialog used to preview files
- modalId: React.PropTypes.string.isRequired,
-
// the channel that this is part of
channelId: React.PropTypes.string,
diff --git a/web/react/components/file_upload.jsx b/web/react/components/file_upload.jsx
index 3cb284171..3dc4e5de2 100644
--- a/web/react/components/file_upload.jsx
+++ b/web/react/components/file_upload.jsx
@@ -110,7 +110,7 @@ export default class FileUpload extends React.Component {
if (typeof files !== 'string' && files.length) {
this.uploadFiles(files);
} else {
- this.props.onUploadError('Invalid file upload', -1);
+ this.props.onTextDrop(e.originalEvent.dataTransfer.getData('Text'));
}
}
@@ -266,6 +266,7 @@ FileUpload.propTypes = {
getFileCount: React.PropTypes.func,
onFileUpload: React.PropTypes.func,
onUploadStart: React.PropTypes.func,
+ onTextDrop: React.PropTypes.func,
channelId: React.PropTypes.string,
postType: React.PropTypes.string
};
diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx
index da9874b0b..bdb50cd9e 100644
--- a/web/react/components/navbar.jsx
+++ b/web/react/components/navbar.jsx
@@ -262,7 +262,7 @@ export default class Navbar extends React.Component {
return (
<div className='navbar-brand'>
<a
- href='/'
+ href={TeamStore.getCurrentTeamUrl() + '/channels/town-square'}
className='heading'
>
{channelTitle}
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index dbbcdc409..6e98e4aba 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -141,7 +141,6 @@ export default class PostBody extends React.Component {
fileAttachmentHolder = (
<FileAttachmentList
filenames={filenames}
- modalId={`view_image_modal_${post.id}`}
channelId={post.channel_id}
userId={post.user_id}
/>
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index d2a0a4035..824e7ef39 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -5,6 +5,8 @@ var UserStore = require('../stores/user_store.jsx');
var utils = require('../utils/utils.jsx');
var Constants = require('../utils/constants.jsx');
+var Tooltip = ReactBootstrap.Tooltip;
+var OverlayTrigger = ReactBootstrap.OverlayTrigger;
export default class PostInfo extends React.Component {
constructor(props) {
@@ -148,15 +150,19 @@ export default class PostInfo extends React.Component {
var dropdown = this.createDropdown();
+ let tooltip = <Tooltip>{utils.displayDate(post.create_at)} at ${utils.displayTime(post.create_at)}</Tooltip>;
+
return (
<ul className='post-header post-info'>
<li className='post-header-col'>
- <time
- className='post-profile-time'
- title={new Date(post.create_at).toString()}
+ <OverlayTrigger
+ placement='top'
+ overlay={tooltip}
>
- {utils.displayDateTime(post.create_at)}
- </time>
+ <time className='post-profile-time'>
+ {utils.displayDateTime(post.create_at)}
+ </time>
+ </OverlayTrigger>
</li>
<li className='post-header-col post-header__reply'>
<div className='dropdown'>
diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx
index 218922b67..3e1e075bb 100644
--- a/web/react/components/post_list.jsx
+++ b/web/react/components/post_list.jsx
@@ -105,18 +105,18 @@ export default class PostList extends React.Component {
UserStore.addStatusesChangeListener(this.onTimeChange);
SocketStore.addChangeListener(this.onSocketChange);
- var postHolder = $(React.findDOMNode(this.refs.postlist));
+ const postHolder = $(React.findDOMNode(this.refs.postlist));
- $(window).on('resize.' + this.props.channelId, function resize() {
+ $(window).resize(() => {
this.resize();
if (!this.scrolled) {
this.scrollToBottom();
}
- }.bind(this));
+ });
- postHolder.on('scroll', function scroll() {
- var position = postHolder.scrollTop() + postHolder.height() + 14;
- var bottom = postHolder[0].scrollHeight;
+ postHolder.on('scroll', () => {
+ const position = postHolder.scrollTop() + postHolder.height() + 14;
+ const bottom = postHolder[0].scrollHeight;
if (position >= bottom) {
this.scrolled = false;
@@ -128,7 +128,7 @@ export default class PostList extends React.Component {
this.userHasSeenNew = true;
}
this.isUserScroll = true;
- }.bind(this));
+ });
$('.post-list__content div .post').removeClass('post--last');
$('.post-list__content div:last-child .post').addClass('post--last');
@@ -146,7 +146,7 @@ export default class PostList extends React.Component {
UserStore.removeStatusesChangeListener(this.onTimeChange);
SocketStore.removeChangeListener(this.onSocketChange);
$('body').off('click.userpopover');
- $(window).off('resize.' + this.props.channelId);
+ $(window).off('resize');
var postHolder = $(React.findDOMNode(this.refs.postlist));
postHolder.off('scroll');
}
diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx
index 4d1892a69..5b4694eb1 100644
--- a/web/react/components/rhs_comment.jsx
+++ b/web/react/components/rhs_comment.jsx
@@ -163,7 +163,6 @@ export default class RhsComment extends React.Component {
fileAttachment = (
<FileAttachmentList
filenames={post.filenames}
- modalId={'rhs_comment_view_image_modal_' + post.id}
channelId={post.channel_id}
userId={post.user_id}
/>
@@ -186,10 +185,7 @@ export default class RhsComment extends React.Component {
<strong><UserProfile userId={post.user_id} /></strong>
</li>
<li className='post-header-col'>
- <time
- className='post-profile-time'
- title={new Date(post.create_at).toString()}
- >
+ <time className='post-profile-time'>
{Utils.displayCommentDateTime(post.create_at)}
</time>
</li>
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx
index e661bdce1..13ab0c982 100644
--- a/web/react/components/rhs_root_post.jsx
+++ b/web/react/components/rhs_root_post.jsx
@@ -111,7 +111,6 @@ export default class RhsRootPost extends React.Component {
fileAttachment = (
<FileAttachmentList
filenames={post.filenames}
- modalId={'rhs_view_image_modal_' + post.id}
channelId={post.channel_id}
userId={post.user_id}
/>
@@ -133,10 +132,7 @@ export default class RhsRootPost extends React.Component {
<ul className='post-header'>
<li className='post-header-col'><strong><UserProfile userId={post.user_id} /></strong></li>
<li className='post-header-col'>
- <time
- className='post-profile-time'
- title={new Date(post.create_at).toString()}
- >
+ <time className='post-profile-time'>
{utils.displayCommentDateTime(post.create_at)}
</time>
</li>
diff --git a/web/react/components/settings_sidebar.jsx b/web/react/components/settings_sidebar.jsx
index e5cbd6e92..4c4675788 100644
--- a/web/react/components/settings_sidebar.jsx
+++ b/web/react/components/settings_sidebar.jsx
@@ -7,7 +7,8 @@ export default class SettingsSidebar extends React.Component {
this.handleClick = this.handleClick.bind(this);
}
- handleClick(tab) {
+ handleClick(tab, e) {
+ e.preventDefault();
this.props.updateTab(tab.name);
$('.settings-modal').addClass('display--content');
}
diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx
index 8311747ee..495159efc 100644
--- a/web/react/components/signup_user_complete.jsx
+++ b/web/react/components/signup_user_complete.jsx
@@ -84,7 +84,7 @@ export default class SignupUserComplete extends React.Component {
if (this.props.hash > 0) {
BrowserStore.setGlobalItem(this.props.hash, JSON.stringify({wizard: 'finished'}));
}
- window.location.href = '/';
+ window.location.href = '/' + this.props.teamName + '/channels/town-square';
}.bind(this),
function emailLoginFailure(err) {
if (err.message === 'Login failed because email address has not been verified') {
diff --git a/web/react/components/team_feature_tab.jsx b/web/react/components/team_feature_tab.jsx
deleted file mode 100644
index 3251746b8..000000000
--- a/web/react/components/team_feature_tab.jsx
+++ /dev/null
@@ -1,190 +0,0 @@
-// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-var SettingItemMin = require('./setting_item_min.jsx');
-var SettingItemMax = require('./setting_item_max.jsx');
-
-var Client = require('../utils/client.jsx');
-var AsyncClient = require('../utils/async_client.jsx');
-
-export default class FeatureTab extends React.Component {
- constructor(props) {
- super(props);
-
- this.submitValetFeature = this.submitValetFeature.bind(this);
- this.handleValetRadio = this.handleValetRadio.bind(this);
- this.onUpdateSection = this.onUpdateSection.bind(this);
- this.setupInitialState = this.setupInitialState.bind(this);
-
- this.state = this.setupInitialState();
- }
- componentWillReceiveProps(newProps) {
- var team = newProps.team;
-
- var allowValet = 'false';
- if (team && team.allow_valet) {
- allowValet = 'true';
- }
-
- this.setState({allowValet: allowValet});
- }
- submitValetFeature() {
- var data = {};
- data.allow_valet = this.state.allowValet;
-
- Client.updateValetFeature(data,
- function success() {
- this.props.updateSection('');
- AsyncClient.getMyTeam();
- }.bind(this),
- function fail(err) {
- var state = this.setupInitialState();
- state.serverError = err;
- this.setState(state);
- }.bind(this)
- );
- }
- handleValetRadio(val) {
- this.setState({allowValet: val});
- React.findDOMNode(this.refs.wrapper).focus();
- }
- onUpdateSection(e) {
- e.preventDefault();
- if (this.props.activeSection === 'valet') {
- this.props.updateSection('');
- } else {
- this.props.updateSection('valet');
- }
- }
- setupInitialState() {
- var allowValet;
- var team = this.props.team;
-
- if (team && team.allow_valet) {
- allowValet = 'true';
- } else {
- allowValet = 'false';
- }
-
- return {allowValet: allowValet};
- }
- render() {
- var clientError = null;
- var serverError = null;
- if (this.state.clientError) {
- clientError = this.state.clientError;
- }
- if (this.state.serverError) {
- serverError = this.state.serverError;
- }
-
- var valetSection;
-
- if (this.props.activeSection === 'valet') {
- var valetActive = [false, false];
- if (this.state.allowValet === 'false') {
- valetActive[1] = true;
- } else {
- valetActive[0] = true;
- }
-
- let inputs = [];
-
- inputs.push(
- <div key='teamValetSetting'>
- <div className='radio'>
- <label>
- <input
- type='radio'
- checked={valetActive[0]}
- onChange={this.handleValetRadio.bind(this, 'true')}
- >
- On
- </input>
- </label>
- <br/>
- </div>
- <div className='radio'>
- <label>
- <input
- type='radio'
- checked={valetActive[1]}
- onChange={this.handleValetRadio.bind(this, 'false')}
- >
- Off
- </input>
- </label>
- <br/>
- </div>
- <div><br/>Valet is a preview feature for enabling a non-user account limited to basic member permissions that can be manipulated by 3rd parties.<br/><br/>IMPORTANT: The preview version of Valet should not be used without a secure connection and a trusted 3rd party, since user credentials are used to connect. OAuth2 will be used in the final release.</div>
- </div>
- );
-
- valetSection = (
- <SettingItemMax
- title='Valet (Preview - EXPERTS ONLY)'
- inputs={inputs}
- submit={this.submitValetFeature}
- server_error={serverError}
- client_error={clientError}
- updateSection={this.onUpdateSection}
- />
- );
- } else {
- var describe = '';
- if (this.state.allowValet === 'false') {
- describe = 'Off';
- } else {
- describe = 'On';
- }
-
- valetSection = (
- <SettingItemMin
- title='Valet (Preview - EXPERTS ONLY)'
- describe={describe}
- updateSection={this.onUpdateSection}
- />
- );
- }
-
- return (
- <div>
- <div className='modal-header'>
- <button
- type='button'
- className='close'
- data-dismiss='modal'
- aria-label='Close'
- >
- <span aria-hidden='true'>&times;</span>
- </button>
- <h4
- className='modal-title'
- ref='title'
- >
- <i className='modal-back'></i>Advanced Features
- </h4>
- </div>
- <div
- ref='wrapper'
- className='user-settings'
- >
- <h3 className='tab-header'>Advanced Features</h3>
- <div className='divider-dark first'/>
- {valetSection}
- <div className='divider-dark'/>
- </div>
- </div>
- );
- }
-}
-
-FeatureTab.defaultProps = {
- team: {},
- activeSection: ''
-};
-FeatureTab.propTypes = {
- updateSection: React.PropTypes.func.isRequired,
- team: React.PropTypes.object.isRequired,
- activeSection: React.PropTypes.string.isRequired
-};
diff --git a/web/react/components/team_import_tab.jsx b/web/react/components/team_import_tab.jsx
index 8315430e4..79f03510f 100644
--- a/web/react/components/team_import_tab.jsx
+++ b/web/react/components/team_import_tab.jsx
@@ -35,7 +35,7 @@ export default class TeamImportTab extends React.Component {
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.'}</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>
</div>
);
diff --git a/web/react/components/team_settings.jsx b/web/react/components/team_settings.jsx
index 396521af9..e91aa20bc 100644
--- a/web/react/components/team_settings.jsx
+++ b/web/react/components/team_settings.jsx
@@ -4,7 +4,6 @@
var TeamStore = require('../stores/team_store.jsx');
var ImportTab = require('./team_import_tab.jsx');
var ExportTab = require('./team_export_tab.jsx');
-var FeatureTab = require('./team_feature_tab.jsx');
var GeneralTab = require('./team_general_tab.jsx');
var Utils = require('../utils/utils.jsx');
@@ -25,7 +24,7 @@ export default class TeamSettings extends React.Component {
onChange() {
var team = TeamStore.getCurrent();
if (!Utils.areStatesEqual(this.state.team, team)) {
- this.setState({team: team});
+ this.setState({team});
}
}
render() {
@@ -43,17 +42,6 @@ export default class TeamSettings extends React.Component {
</div>
);
break;
- case 'feature':
- result = (
- <div>
- <FeatureTab
- team={this.state.team}
- activeSection={this.props.activeSection}
- updateSection={this.props.updateSection}
- />
- </div>
- );
- break;
case 'import':
result = (
<div>
diff --git a/web/react/components/team_settings_modal.jsx b/web/react/components/team_settings_modal.jsx
index 0513c811f..a96aadccf 100644
--- a/web/react/components/team_settings_modal.jsx
+++ b/web/react/components/team_settings_modal.jsx
@@ -20,8 +20,8 @@ export default class TeamSettingsModal extends React.Component {
$('body').on('click', '.modal-back', function handleBackClick() {
$(this).closest('.modal-dialog').removeClass('display--content');
});
- $('body').on('click', '.modal-header .close', function handleCloseClick() {
- setTimeout(function removeContent() {
+ $('body').on('click', '.modal-header .close', () => {
+ setTimeout(() => {
$('.modal-dialog.display--content').removeClass('display--content');
}, 500);
});
@@ -33,11 +33,12 @@ export default class TeamSettingsModal extends React.Component {
this.setState({activeSection: section});
}
render() {
- let tabs = [];
+ const tabs = [];
tabs.push({name: 'general', uiName: 'General', icon: 'glyphicon glyphicon-cog'});
tabs.push({name: 'import', uiName: 'Import', icon: 'glyphicon glyphicon-upload'});
- tabs.push({name: 'export', uiName: 'Export', icon: 'glyphicon glyphicon-download'});
- tabs.push({name: 'feature', uiName: 'Advanced', icon: 'glyphicon glyphicon-wrench'});
+
+ // To enable export uncomment this line
+ //tabs.push({name: 'export', uiName: 'Export', icon: 'glyphicon glyphicon-download'});
return (
<div
@@ -63,7 +64,7 @@ export default class TeamSettingsModal extends React.Component {
className='modal-title'
ref='title'
>
- Team Settings
+ {'Team Settings'}
</h4>
</div>
<div className='modal-body'>
diff --git a/web/react/components/team_signup_password_page.jsx b/web/react/components/team_signup_password_page.jsx
index b26d9f6ce..105e4817a 100644
--- a/web/react/components/team_signup_password_page.jsx
+++ b/web/react/components/team_signup_password_page.jsx
@@ -53,7 +53,7 @@ export default class TeamSignupPasswordPage extends React.Component {
props.state.wizard = 'finished';
props.updateParent(props.state, true);
- window.location.href = '/';
+ window.location.href = '/' + teamSignup.team.name + '/channels/town-square';
}.bind(this),
function loginFail(err) {
if (err.message === 'Login failed because email address has not been verified') {
diff --git a/web/react/components/team_signup_with_sso.jsx b/web/react/components/team_signup_with_sso.jsx
index 2849b4cbb..a4972dd8d 100644
--- a/web/react/components/team_signup_with_sso.jsx
+++ b/web/react/components/team_signup_with_sso.jsx
@@ -42,7 +42,7 @@ export default class SSOSignUpPage extends React.Component {
if (data.follow_link) {
window.location.href = data.follow_link;
} else {
- window.location.href = '/';
+ window.location.href = '/' + team.name + '/channels/town-square';
}
},
function fail(err) {
diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx
index ea8126bec..5f5316013 100644
--- a/web/react/components/textbox.jsx
+++ b/web/react/components/textbox.jsx
@@ -5,7 +5,6 @@ const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
const PostStore = require('../stores/post_store.jsx');
const CommandList = require('./command_list.jsx');
const ErrorStore = require('../stores/error_store.jsx');
-const AsyncClient = require('../utils/async_client.jsx');
const Utils = require('../utils/utils.jsx');
const Constants = require('../utils/constants.jsx');
@@ -18,7 +17,6 @@ export default class Textbox extends React.Component {
this.getStateFromStores = this.getStateFromStores.bind(this);
this.onListenerChange = this.onListenerChange.bind(this);
this.onRecievedError = this.onRecievedError.bind(this);
- this.onTimerInterrupt = this.onTimerInterrupt.bind(this);
this.updateMentionTab = this.updateMentionTab.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
@@ -35,8 +33,7 @@ export default class Textbox extends React.Component {
this.state = {
mentionText: '-1',
mentions: [],
- connection: '',
- timerInterrupt: null
+ connection: ''
};
this.caret = -1;
@@ -44,6 +41,7 @@ export default class Textbox extends React.Component {
this.doProcessMentions = false;
this.mentions = [];
}
+
getStateFromStores() {
const error = ErrorStore.getLastError();
@@ -53,6 +51,7 @@ export default class Textbox extends React.Component {
return {message: null};
}
+
componentDidMount() {
PostStore.addAddMentionListener(this.onListenerChange);
ErrorStore.addChangeListener(this.onRecievedError);
@@ -60,46 +59,28 @@ export default class Textbox extends React.Component {
this.resize();
this.updateMentionTab(null);
}
+
componentWillUnmount() {
PostStore.removeAddMentionListener(this.onListenerChange);
ErrorStore.removeChangeListener(this.onRecievedError);
}
+
onListenerChange(id, username) {
if (id === this.props.id) {
this.addMention(username);
}
}
- onRecievedError() {
- const errorState = this.getStateFromStores();
- if (this.state.timerInterrupt !== null) {
- window.clearInterval(this.state.timerInterrupt);
- this.setState({timerInterrupt: null});
- }
+ onRecievedError() {
+ const errorState = ErrorStore.getLastError();
- if (errorState.message === 'There appears to be a problem with your internet connection') {
+ if (errorState && errorState.connErrorCount > 0) {
this.setState({connection: 'bad-connection'});
- const timerInterrupt = window.setInterval(this.onTimerInterrupt, 5000);
- this.setState({timerInterrupt: timerInterrupt});
} else {
this.setState({connection: ''});
}
}
- onTimerInterrupt() {
- // Since these should only happen when you have no connection and slightly briefly after any
- // performance hit should not matter
- if (this.state.connection === 'bad-connection') {
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_ERROR,
- err: null
- });
-
- AsyncClient.updateLastViewedAt();
- }
- window.clearInterval(this.state.timerInterrupt);
- this.setState({timerInterrupt: null});
- }
componentDidUpdate() {
if (this.caret >= 0) {
Utils.setCaretPosition(React.findDOMNode(this.refs.message), this.caret);
@@ -111,6 +92,7 @@ export default class Textbox extends React.Component {
}
this.resize();
}
+
componentWillReceiveProps(nextProps) {
if (!this.addedMention) {
this.checkForNewMention(nextProps.messageText);
@@ -122,19 +104,22 @@ export default class Textbox extends React.Component {
this.addedMention = false;
this.refs.commands.getSuggestedCommands(nextProps.messageText);
}
+
updateMentionTab(mentionText) {
// using setTimeout so dispatch isn't called during an in progress dispatch
- setTimeout(function updateMentionTabAfterTimeout() {
+ setTimeout(() => {
AppDispatcher.handleViewAction({
type: ActionTypes.RECIEVED_MENTION_DATA,
id: this.props.id,
mention_text: mentionText
});
- }.bind(this), 1);
+ }, 1);
}
+
handleChange() {
this.props.onUserInput(React.findDOMNode(this.refs.message).value);
}
+
handleKeyPress(e) {
const text = React.findDOMNode(this.refs.message).value;
@@ -157,6 +142,7 @@ export default class Textbox extends React.Component {
this.props.onKeyPress(e);
}
+
handleKeyDown(e) {
if (Utils.getSelectedText(React.findDOMNode(this.refs.message)) !== '') {
this.doProcessMentions = true;
@@ -166,6 +152,7 @@ export default class Textbox extends React.Component {
this.handleBackspace(e);
}
}
+
handleBackspace() {
const text = React.findDOMNode(this.refs.message).value;
if (text.indexOf('/') === 0) {
@@ -185,6 +172,7 @@ export default class Textbox extends React.Component {
this.doProcessMentions = true;
}
}
+
checkForNewMention(text) {
const caret = Utils.getCaretPosition(React.findDOMNode(this.refs.message));
@@ -211,6 +199,7 @@ export default class Textbox extends React.Component {
const name = preText.substring(atIndex + 1, preText.length).toLowerCase();
this.updateMentionTab(name);
}
+
addMention(name) {
const caret = Utils.getCaretPosition(React.findDOMNode(this.refs.message));
@@ -233,11 +222,13 @@ export default class Textbox extends React.Component {
this.props.onUserInput(`${prefix}@${name} ${suffix}`);
}
+
addCommand(cmd) {
const elm = React.findDOMNode(this.refs.message);
elm.value = cmd;
this.handleChange();
}
+
resize() {
const e = React.findDOMNode(this.refs.message);
const w = React.findDOMNode(this.refs.wrapper);
@@ -264,21 +255,25 @@ export default class Textbox extends React.Component {
this.props.onHeightChange();
}
}
+
handleFocus() {
const elm = React.findDOMNode(this.refs.message);
if (elm.title === elm.value) {
elm.value = '';
}
}
+
handleBlur() {
const elm = React.findDOMNode(this.refs.message);
if (elm.value === '') {
elm.value = elm.title;
}
}
+
handlePaste() {
this.doProcessMentions = true;
}
+
render() {
return (
<div
diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx
index 44630a318..c680d75d1 100644
--- a/web/react/components/user_settings/custom_theme_chooser.jsx
+++ b/web/react/components/user_settings/custom_theme_chooser.jsx
@@ -53,9 +53,12 @@ export default class CustomThemeChooser extends React.Component {
const elements = [];
let colors = '';
- Constants.THEME_ELEMENTS.forEach((element) => {
+ Constants.THEME_ELEMENTS.forEach((element, index) => {
elements.push(
- <div className='col-sm-4 form-group'>
+ <div
+ className='col-sm-4 form-group'
+ key={'custom-theme-key' + index}
+ >
<label className='custom-label'>{element.uiName}</label>
<div
className='input-group color-picker'
diff --git a/web/react/components/user_settings/manage_incoming_hooks.jsx b/web/react/components/user_settings/manage_incoming_hooks.jsx
index df089a403..1bbfbd162 100644
--- a/web/react/components/user_settings/manage_incoming_hooks.jsx
+++ b/web/react/components/user_settings/manage_incoming_hooks.jsx
@@ -107,23 +107,23 @@ export default class ManageIncomingHooks extends React.Component {
this.state.hooks.forEach((hook) => {
const c = ChannelStore.get(hook.channel_id);
hooks.push(
- <div>
- <div className='divider-light'></div>
- <span>
- <strong>{'URL: '}</strong>{Utils.getWindowLocationOrigin() + '/hooks/' + hook.id}
- </span>
- <br/>
- <span>
+ <div className='font--small'>
+ <div className='padding-top x2 divider-light'></div>
+ <div className='padding-top x2'>
+ <strong>{'URL: '}</strong><span className='word-break--all'>{Utils.getWindowLocationOrigin() + '/hooks/' + hook.id}</span>
+ </div>
+ <div className='padding-top'>
<strong>{'Channel: '}</strong>{c.name}
- </span>
- <br/>
- <a
- className={'btn btn-sm btn-primary'}
- href='#'
- onClick={this.removeHook.bind(this, hook.id)}
- >
- {'Remove'}
- </a>
+ </div>
+ <div className='padding-top'>
+ <a
+ className={'text-danger'}
+ href='#'
+ onClick={this.removeHook.bind(this, hook.id)}
+ >
+ {'Remove'}
+ </a>
+ </div>
</div>
);
});
@@ -134,41 +134,38 @@ export default class ManageIncomingHooks extends React.Component {
} else if (hooks.length > 0) {
displayHooks = hooks;
} else {
- displayHooks = <label>{'None'}</label>;
+ displayHooks = <label>{': None'}</label>;
}
const existingHooks = (
- <div>
- <label className='control-label'>{'Existing incoming webhooks'}</label>
- <br/>
+ <div className='padding-top x2'>
+ <label className='control-label padding-top x2'>{'Existing incoming webhooks'}</label>
{displayHooks}
</div>
);
return (
- <div
- key='addIncomingHook'
- className='form-group'
- >
+ <div key='addIncomingHook'>
<label className='control-label'>{'Add a new incoming webhook'}</label>
- <br/>
- <div>
+ <div className='padding-top'>
<select
ref='channelName'
+ className='form-control'
value={this.state.channelId}
onChange={this.updateChannelId}
>
{options}
</select>
- <br/>
{serverError}
- <a
- className={'btn btn-sm btn-primary' + disableButton}
- href='#'
- onClick={this.addNewHook}
- >
- {'Add'}
- </a>
+ <div className='padding-top'>
+ <a
+ className={'btn btn-sm btn-primary' + disableButton}
+ href='#'
+ onClick={this.addNewHook}
+ >
+ {'Add'}
+ </a>
+ </div>
</div>
{existingHooks}
</div>
diff --git a/web/react/components/user_settings/premade_theme_chooser.jsx b/web/react/components/user_settings/premade_theme_chooser.jsx
index e6aa2f5b9..f8f916bd0 100644
--- a/web/react/components/user_settings/premade_theme_chooser.jsx
+++ b/web/react/components/user_settings/premade_theme_chooser.jsx
@@ -23,7 +23,10 @@ export default class PremadeThemeChooser extends React.Component {
}
premadeThemes.push(
- <div className='col-sm-3 premade-themes'>
+ <div
+ className='col-sm-3 premade-themes'
+ key={'premade-theme-key' + k}
+ >
<div
className={activeClass}
onClick={() => this.props.updateTheme(premadeTheme)}
diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx
index 7617f04d1..4372069e7 100644
--- a/web/react/components/user_settings/user_settings_appearance.jsx
+++ b/web/react/components/user_settings/user_settings_appearance.jsx
@@ -81,6 +81,8 @@ export default class UserSettingsAppearance extends React.Component {
$('#user_settings').off('hidden.bs.modal', this.handleClose);
this.props.updateTab('general');
+ $('.ps-container.modal-body').scrollTop(0);
+ $('.ps-container.modal-body').perfectScrollbar('update');
$('#user_settings').modal('hide');
},
(err) => {
diff --git a/web/react/components/user_settings/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx
index 8d364cde7..ba14f019f 100644
--- a/web/react/components/user_settings/user_settings_notifications.jsx
+++ b/web/react/components/user_settings/user_settings_notifications.jsx
@@ -265,9 +265,12 @@ export default class NotificationsTab extends React.Component {
e.preventDefault();
}.bind(this);
+ const extraInfo = <span>{'Desktop notifications are available on Firefox, Safari, and Chrome.'}</span>;
+
desktopSection = (
<SettingItemMax
title='Send desktop notifications'
+ extraInfo={extraInfo}
inputs={inputs}
submit={this.handleSubmit}
server_error={serverError}
@@ -343,9 +346,12 @@ export default class NotificationsTab extends React.Component {
e.preventDefault();
}.bind(this);
+ const extraInfo = <span>{'Desktop notification sounds are available on Firefox, Safari, Chrome, Internet Explorer, and Edge.'}</span>;
+
soundSection = (
<SettingItemMax
title='Desktop notification sounds'
+ extraInfo={extraInfo}
inputs={inputs}
submit={this.handleSubmit}
server_error={serverError}
diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx
index dafcdd9f9..a7fecb689 100644
--- a/web/react/components/view_image.jsx
+++ b/web/react/components/view_image.jsx
@@ -1,8 +1,11 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var Client = require('../utils/client.jsx');
-var Utils = require('../utils/utils.jsx');
+const Client = require('../utils/client.jsx');
+const Utils = require('../utils/utils.jsx');
+const Constants = require('../utils/constants.jsx');
+const ViewImagePopoverBar = require('./view_image_popover_bar.jsx');
+const Modal = ReactBootstrap.Modal;
export default class ViewImageModal extends React.Component {
constructor(props) {
@@ -16,6 +19,10 @@ export default class ViewImageModal extends React.Component {
this.handleKeyPress = this.handleKeyPress.bind(this);
this.getPublicLink = this.getPublicLink.bind(this);
this.getPreviewImagePath = this.getPreviewImagePath.bind(this);
+ this.onModalShown = this.onModalShown.bind(this);
+ this.onModalHidden = this.onModalHidden.bind(this);
+ this.onMouseEnterImage = this.onMouseEnterImage.bind(this);
+ this.onMouseLeaveImage = this.onMouseLeaveImage.bind(this);
var loaded = [];
var progress = [];
@@ -23,9 +30,20 @@ export default class ViewImageModal extends React.Component {
loaded.push(false);
progress.push(0);
}
- this.state = {imgId: this.props.startId, viewed: false, loaded: loaded, progress: progress, images: {}, fileSizes: {}};
+ this.state = {
+ imgId: this.props.startId,
+ imgHeight: '100%',
+ loaded: loaded,
+ progress: progress,
+ images: {},
+ fileSizes: {},
+ showFooter: false
+ };
}
- handleNext() {
+ handleNext(e) {
+ if (e) {
+ e.stopPropagation();
+ }
var id = this.state.imgId + 1;
if (id > this.props.filenames.length - 1) {
id = 0;
@@ -33,7 +51,10 @@ export default class ViewImageModal extends React.Component {
this.setState({imgId: id});
this.loadImage(id);
}
- handlePrev() {
+ handlePrev(e) {
+ if (e) {
+ e.stopPropagation();
+ }
var id = this.state.imgId - 1;
if (id < 0) {
id = this.props.filenames.length - 1;
@@ -50,15 +71,27 @@ export default class ViewImageModal extends React.Component {
this.handlePrev();
}
}
- componentWillReceiveProps(nextProps) {
+ onModalShown(nextProps) {
this.setState({imgId: nextProps.startId});
+ this.loadImage(nextProps.startId);
+ }
+ onModalHidden() {
+ if (this.refs.video) {
+ var video = React.findDOMNode(this.refs.video);
+ video.pause();
+ video.currentTime = 0;
+ }
+ }
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.show === true && this.props.show === false) {
+ this.onModalShown(nextProps);
+ } else if (nextProps.show === false && this.props.show === true) {
+ this.onModalHidden();
+ }
}
loadImage(id) {
var imgHeight = $(window).height() - 100;
- if (this.state.loaded[id] || this.state.images[id]) {
- $('.modal .modal-image .image-wrapper img').css('max-height', imgHeight);
- return;
- }
+ this.setState({imgHeight});
var filename = this.props.filenames[id];
@@ -68,84 +101,27 @@ export default class ViewImageModal extends React.Component {
if (fileType === 'image') {
var img = new Image();
img.load(this.getPreviewImagePath(filename),
- function load() {
- var progress = this.state.progress;
- progress[id] = img.completedPercentage;
- this.setState({progress: progress});
- }.bind(this));
- img.onload = (function onload(imgid) {
- return function onloadReturn() {
- var loaded = this.state.loaded;
- loaded[imgid] = true;
- this.setState({loaded: loaded});
- $(React.findDOMNode(this.refs.image)).css('max-height', imgHeight);
- }.bind(this);
- }.bind(this)(id));
+ () => {
+ const progress = this.state.progress;
+ progress[id] = img.completedPercentage;
+ this.setState({progress});
+ });
+ img.onload = () => {
+ const loaded = this.state.loaded;
+ loaded[id] = true;
+ this.setState({loaded});
+ };
var images = this.state.images;
images[id] = img;
- this.setState({images: images});
+ this.setState({images});
} else {
// there's nothing to load for non-image files
var loaded = this.state.loaded;
loaded[id] = true;
- this.setState({loaded: loaded});
- }
- }
- componentDidUpdate() {
- if (this.state.loaded[this.state.imgId]) {
- if (this.refs.imageWrap) {
- $(React.findDOMNode(this.refs.imageWrap)).removeClass('default');
- }
+ this.setState({loaded});
}
}
componentDidMount() {
- $('#' + this.props.modalId).on('shown.bs.modal', function onModalShow() {
- this.setState({viewed: true});
- this.loadImage(this.state.imgId);
- }.bind(this));
-
- $('#' + this.props.modalId).on('hidden.bs.modal', function onModalHide() {
- if (this.refs.video) {
- var video = React.findDOMNode(this.refs.video);
- video.pause();
- video.currentTime = 0;
- }
- }.bind(this));
-
- $(React.findDOMNode(this.refs.modal)).click(function onModalClick(e) {
- if (e.target === this || e.target === React.findDOMNode(this.refs.imageBody)) {
- $('.image_modal').modal('hide');
- }
- }.bind(this));
-
- $(React.findDOMNode(this.refs.imageWrap)).hover(
- function onModalHover() {
- $(React.findDOMNode(this.refs.imageFooter)).addClass('footer--show');
- }.bind(this), function offModalHover() {
- $(React.findDOMNode(this.refs.imageFooter)).removeClass('footer--show');
- }.bind(this)
- );
-
- if (this.refs.previewArrowLeft) {
- $(React.findDOMNode(this.refs.previewArrowLeft)).hover(
- function onModalHover() {
- $(React.findDOMNode(this.refs.imageFooter)).addClass('footer--show');
- }.bind(this), function offModalHover() {
- $(React.findDOMNode(this.refs.imageFooter)).removeClass('footer--show');
- }.bind(this)
- );
- }
-
- if (this.refs.previewArrowRight) {
- $(React.findDOMNode(this.refs.previewArrowRight)).hover(
- function onModalHover() {
- $(React.findDOMNode(this.refs.imageFooter)).addClass('footer--show');
- }.bind(this), function offModalHover() {
- $(React.findDOMNode(this.refs.imageFooter)).removeClass('footer--show');
- }.bind(this)
- );
- }
-
$(window).on('keyup', this.handleKeyPress);
// keep track of whether or not this component is mounted so we can safely set the state asynchronously
@@ -189,6 +165,12 @@ export default class ViewImageModal extends React.Component {
// only images have proper previews, so just use a placeholder icon for non-images
return Utils.getPreviewImagePathForFileType(fileType);
}
+ onMouseEnterImage() {
+ this.setState({showFooter: true});
+ }
+ onMouseLeaveImage() {
+ this.setState({showFooter: false});
+ }
render() {
if (this.props.filenames.length < 1 || this.props.filenames.length - 1 < this.state.imgId) {
return <div/>;
@@ -219,11 +201,20 @@ export default class ViewImageModal extends React.Component {
</a>
);
} else if (fileType === 'video' || fileType === 'audio') {
+ let width = Constants.WEB_VIDEO_WIDTH;
+ let height = Constants.WEB_VIDEO_HEIGHT;
+ if (Utils.isMobile()) {
+ width = Constants.MOBILE_VIDEO_WIDTH;
+ height = Constants.MOBILE_VIDEO_HEIGHT;
+ }
+
content = (
<video
ref='video'
data-setup='{}'
controls='controls'
+ width={width}
+ height={height}
>
<source src={Utils.getWindowLocationOrigin() + '/api/v1/files/get' + filename} />
</video>
@@ -299,23 +290,6 @@ export default class ViewImageModal extends React.Component {
bgClass = 'black-bg';
}
- var publicLink = '';
- if (global.window.config.EnablePublicLink === 'true') {
- publicLink = (
- <div>
- <a
- href='#'
- className='public-link text'
- data-title='Public Image'
- onClick={this.getPublicLink}
- >
- Get Public Link
- </a>
- <span className='text'> | </span>
- </div>
- );
- }
-
var leftArrow = '';
var rightArrow = '';
if (this.props.filenames.length > 1) {
@@ -342,65 +316,61 @@ export default class ViewImageModal extends React.Component {
);
}
+ let closeButtonClass = 'modal-close';
+ if (this.state.showFooter) {
+ closeButtonClass += ' modal-close--show';
+ }
+
return (
- <div
- className='modal fade image_modal'
- ref='modal'
- id={this.props.modalId}
- tabIndex='-1'
- role='dialog'
- aria-hidden='true'
+ <Modal
+ show={this.props.show}
+ onHide={this.props.onModalDismissed}
+ className='image_modal'
+ dialogClassName='modal-image'
>
- <div className='modal-dialog modal-image'>
- <div className='modal-content image-content'>
+ <Modal.Body
+ modalClassName='image-body'
+ onClick={this.props.onModalDismissed}
+ >
+ <div
+ className={'image-wrapper ' + bgClass}
+ style={{maxHeight: this.state.imgHeight}}
+ onMouseEnter={this.onMouseEnterImage}
+ onMouseLeave={this.onMouseLeaveImage}
+ onClick={(e) => e.stopPropagation()}
+ >
<div
- ref='imageBody'
- className='modal-body image-body'
- >
- <div
- ref='imageWrap'
- className={'image-wrapper default ' + bgClass}
- >
- <div
- className='modal-close'
- data-dismiss='modal'
- />
- {content}
- <div
- ref='imageFooter'
- className='modal-button-bar'
- >
- <span className='pull-left text'>{'File ' + (this.state.imgId + 1) + ' of ' + this.props.filenames.length}</span>
- <div className='image-links'>
- {publicLink}
- <a
- href={fileUrl}
- download={name}
- className='text'
- >
- Download
- </a>
- </div>
- </div>
- </div>
- {leftArrow}
- {rightArrow}
- </div>
+ className={closeButtonClass}
+ onClick={this.props.onModalDismissed}
+ />
+ {content}
+ <ViewImagePopoverBar
+ show={this.state.showFooter}
+ fileId={this.state.imgId}
+ totalFiles={this.props.filenames.length}
+ filename={name}
+ fileURL={fileUrl}
+ onGetPublicLinkPressed={this.getPublicLink}
+ />
</div>
- </div>
- </div>
+ {leftArrow}
+ {rightArrow}
+ </Modal.Body>
+ </Modal>
);
}
}
ViewImageModal.defaultProps = {
+ show: false,
filenames: [],
- modalId: '',
channelId: '',
userId: '',
startId: 0
};
ViewImageModal.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ onModalDismissed: React.PropTypes.func.isRequired,
filenames: React.PropTypes.array,
modalId: React.PropTypes.string,
channelId: React.PropTypes.string,
diff --git a/web/react/components/view_image_popover_bar.jsx b/web/react/components/view_image_popover_bar.jsx
new file mode 100644
index 000000000..68817d751
--- /dev/null
+++ b/web/react/components/view_image_popover_bar.jsx
@@ -0,0 +1,66 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+export default class ViewImagePopoverBar extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+ render() {
+ var publicLink = '';
+ if (global.window.config.EnablePublicLink === 'true') {
+ publicLink = (
+ <div>
+ <a
+ href='#'
+ className='public-link text'
+ data-title='Public Image'
+ onClick={this.getPublicLink}
+ >
+ {'Get Public Link'}
+ </a>
+ <span className='text'>{' | '}</span>
+ </div>
+ );
+ }
+
+ var footerClass = 'modal-button-bar';
+ if (this.props.show) {
+ footerClass += ' footer--show';
+ }
+
+ return (
+ <div
+ ref='imageFooter'
+ className={footerClass}
+ >
+ <span className='pull-left text'>{'File ' + (this.props.fileId + 1) + ' of ' + this.props.totalFiles}</span>
+ <div className='image-links'>
+ {publicLink}
+ <a
+ href={this.props.fileURL}
+ download={this.props.filename}
+ className='text'
+ >
+ {'Download'}
+ </a>
+ </div>
+ </div>
+ );
+ }
+}
+ViewImagePopoverBar.defaultProps = {
+ show: false,
+ imgId: 0,
+ totalFiles: 0,
+ filename: '',
+ fileURL: ''
+};
+
+ViewImagePopoverBar.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ fileId: React.PropTypes.number.isRequired,
+ totalFiles: React.PropTypes.number.isRequired,
+ filename: React.PropTypes.string.isRequired,
+ fileURL: React.PropTypes.string.isRequired,
+ onGetPublicLinkPressed: React.PropTypes.func.isRequired
+};
diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx
index 07207c556..74259194a 100644
--- a/web/react/pages/channel.jsx
+++ b/web/react/pages/channel.jsx
@@ -26,6 +26,7 @@ var ChannelInviteModal = require('../components/channel_invite_modal.jsx');
var TeamMembersModal = require('../components/team_members.jsx');
var DirectChannelModal = require('../components/more_direct_channels.jsx');
var ErrorBar = require('../components/error_bar.jsx');
+var ErrorStore = require('../stores/error_store.jsx');
var ChannelLoader = require('../components/channel_loader.jsx');
var MentionList = require('../components/mention_list.jsx');
var ChannelInfoModal = require('../components/channel_info_modal.jsx');
@@ -234,6 +235,11 @@ function setupChannelPage(props) {
<RegisterAppModal />,
document.getElementById('register_app_modal')
);
+
+ if (global.window.config.SendEmailNotifications === 'false') {
+ ErrorStore.storeLastError({message: 'Preview Mode: Email notifications have not been configured'});
+ ErrorStore.emitChange();
+ }
}
global.window.setup_channel_page = setupChannelPage;
diff --git a/web/react/stores/admin_store.jsx b/web/react/stores/admin_store.jsx
index dd5b60a24..7b2aeb631 100644
--- a/web/react/stores/admin_store.jsx
+++ b/web/react/stores/admin_store.jsx
@@ -4,11 +4,14 @@
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var EventEmitter = require('events').EventEmitter;
+var BrowserStore = require('../stores/browser_store.jsx');
+
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
var LOG_CHANGE_EVENT = 'log_change';
var CONFIG_CHANGE_EVENT = 'config_change';
+var ALL_TEAMS_EVENT = 'all_team_change';
class AdminStoreClass extends EventEmitter {
constructor() {
@@ -16,6 +19,7 @@ class AdminStoreClass extends EventEmitter {
this.logs = null;
this.config = null;
+ this.teams = null;
this.emitLogChange = this.emitLogChange.bind(this);
this.addLogChangeListener = this.addLogChangeListener.bind(this);
@@ -24,6 +28,10 @@ class AdminStoreClass extends EventEmitter {
this.emitConfigChange = this.emitConfigChange.bind(this);
this.addConfigChangeListener = this.addConfigChangeListener.bind(this);
this.removeConfigChangeListener = this.removeConfigChangeListener.bind(this);
+
+ this.emitAllTeamsChange = this.emitAllTeamsChange.bind(this);
+ this.addAllTeamsChangeListener = this.addAllTeamsChangeListener.bind(this);
+ this.removeAllTeamsChangeListener = this.removeAllTeamsChangeListener.bind(this);
}
emitLogChange() {
@@ -50,6 +58,18 @@ class AdminStoreClass extends EventEmitter {
this.removeListener(CONFIG_CHANGE_EVENT, callback);
}
+ emitAllTeamsChange() {
+ this.emit(ALL_TEAMS_EVENT);
+ }
+
+ addAllTeamsChangeListener(callback) {
+ this.on(ALL_TEAMS_EVENT, callback);
+ }
+
+ removeAllTeamsChangeListener(callback) {
+ this.removeListener(ALL_TEAMS_EVENT, callback);
+ }
+
getLogs() {
return this.logs;
}
@@ -65,6 +85,22 @@ class AdminStoreClass extends EventEmitter {
saveConfig(config) {
this.config = config;
}
+
+ getAllTeams() {
+ return this.teams;
+ }
+
+ saveAllTeams(teams) {
+ this.teams = teams;
+ }
+
+ getSelectedTeams() {
+ return BrowserStore.getItem('seleted_teams');
+ }
+
+ saveSelectedTeams(teams) {
+ BrowserStore.setItem('seleted_teams', teams);
+ }
}
var AdminStore = new AdminStoreClass();
@@ -81,6 +117,10 @@ AdminStoreClass.dispatchToken = AppDispatcher.register((payload) => {
AdminStore.saveConfig(action.config);
AdminStore.emitConfigChange();
break;
+ case ActionTypes.RECIEVED_ALL_TEAMS:
+ AdminStore.saveAllTeams(action.teams);
+ AdminStore.emitAllTeamsChange();
+ break;
default:
}
});
diff --git a/web/react/stores/error_store.jsx b/web/react/stores/error_store.jsx
index 597c88cff..ece7d8522 100644
--- a/web/react/stores/error_store.jsx
+++ b/web/react/stores/error_store.jsx
@@ -48,7 +48,7 @@ class ErrorStoreClass extends EventEmitter {
var ErrorStore = new ErrorStoreClass();
-ErrorStore.dispatchToken = AppDispatcher.register(function registry(payload) {
+ErrorStore.dispatchToken = AppDispatcher.register((payload) => {
var action = payload.action;
switch (action.type) {
case ActionTypes.RECIEVED_ERROR:
diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx
index ae74059d1..1d853f979 100644
--- a/web/react/stores/socket_store.jsx
+++ b/web/react/stores/socket_store.jsx
@@ -3,6 +3,7 @@
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var UserStore = require('./user_store.jsx');
+var ErrorStore = require('./error_store.jsx');
var EventEmitter = require('events').EventEmitter;
var Constants = require('../utils/constants.jsx');
@@ -21,6 +22,7 @@ class SocketStoreClass extends EventEmitter {
this.addChangeListener = this.addChangeListener.bind(this);
this.removeChangeListener = this.removeChangeListener.bind(this);
this.sendMessage = this.sendMessage.bind(this);
+ this.failCount = 0;
this.initialize();
}
@@ -37,27 +39,43 @@ class SocketStoreClass extends EventEmitter {
protocol = 'wss://';
}
var connUrl = protocol + location.host + '/api/v1/websocket';
- console.log('connecting to ' + connUrl); //eslint-disable-line no-console
+ if (this.failCount === 0) {
+ console.log('websocket connecting to ' + connUrl); //eslint-disable-line no-console
+ }
conn = new WebSocket(connUrl);
- conn.onclose = function closeConn(evt) {
- console.log('websocket closed'); //eslint-disable-line no-console
- console.log(evt); //eslint-disable-line no-console
+ conn.onopen = () => {
+ if (this.failCount > 0) {
+ console.log('websocket re-established connection'); //eslint-disable-line no-console
+ }
+
+ this.failCount = 0;
+ ErrorStore.storeLastError(null);
+ ErrorStore.emitChange();
+ };
+
+ conn.onclose = () => {
conn = null;
setTimeout(
- function reconnect() {
+ () => {
this.initialize();
- }.bind(this),
+ },
3000
);
- }.bind(this);
+ };
+
+ conn.onerror = (evt) => {
+ if (this.failCount === 0) {
+ console.log('websocket error ' + evt); //eslint-disable-line no-console
+ }
+
+ this.failCount = this.failCount + 1;
- conn.onerror = function connError(evt) {
- console.log('websocket error'); //eslint-disable-line no-console
- console.log(evt); //eslint-disable-line no-console
+ ErrorStore.storeLastError({connErrorCount: this.failCount, message: 'We cannot reach the Mattermost service. The service may be down or misconfigured. Please contact an administrator to make sure the WebSocket port is configured properly.'});
+ ErrorStore.emitChange();
};
- conn.onmessage = function connMessage(evt) {
+ conn.onmessage = (evt) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_MSG,
msg: JSON.parse(evt.data)
@@ -86,7 +104,7 @@ class SocketStoreClass extends EventEmitter {
var SocketStore = new SocketStoreClass();
-SocketStore.dispatchToken = AppDispatcher.register(function registry(payload) {
+SocketStore.dispatchToken = AppDispatcher.register((payload) => {
var action = payload.action;
switch (action.type) {
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index ed228f6c4..ab2965000 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -371,6 +371,32 @@ export function getConfig() {
);
}
+export function getAllTeams() {
+ if (isCallInProgress('getAllTeams')) {
+ return;
+ }
+
+ callTracker.getAllTeams = utils.getTimestamp();
+ client.getAllTeams(
+ (data, textStatus, xhr) => {
+ callTracker.getAllTeams = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_ALL_TEAMS,
+ teams: data
+ });
+ },
+ (err) => {
+ callTracker.getAllTeams = 0;
+ dispatchError(err, 'getAllTeams');
+ }
+ );
+}
+
export function findTeams(email) {
if (isCallInProgress('findTeams_' + email)) {
return;
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index a19f58e61..715e26197 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -27,7 +27,7 @@ function handleError(methodName, xhr, status, err) {
msg = 'error in ' + methodName + ' status=' + status + ' statusCode=' + xhr.status + ' err=' + err;
if (xhr.status === 0) {
- e = {message: 'There appears to be a problem with your internet connection'};
+ e = {message: 'There appears to be a problem with your internet connection', connErrorCount: 1};
} else {
e = {message: 'We received an unexpected status code from the server (' + xhr.status + ')'};
}
@@ -347,6 +347,20 @@ export function testEmail(config, success, error) {
});
}
+export function getAllTeams(success, error) {
+ $.ajax({
+ url: '/api/v1/teams/all',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getAllTeams', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function getMeSynchronous(success, error) {
var currentUser = null;
$.ajax({
@@ -890,6 +904,21 @@ export function getProfiles(success, error) {
});
}
+export function getProfilesForTeam(teamId, success, error) {
+ $.ajax({
+ cache: false,
+ url: '/api/v1/users/profiles/' + teamId,
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getProfilesForTeam', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function uploadFile(formData, success, error) {
var request = $.ajax({
url: '/api/v1/files/upload',
@@ -1013,23 +1042,6 @@ export function getMyTeam(success, error) {
});
}
-export function updateValetFeature(data, success, error) {
- $.ajax({
- url: '/api/v1/teams/update_valet_feature',
- dataType: 'json',
- contentType: 'application/json',
- type: 'POST',
- data: JSON.stringify(data),
- success,
- error: function onError(xhr, status, err) {
- var e = handleError('updateValetFeature', xhr, status, err);
- error(e);
- }
- });
-
- track('api', 'api_teams_update_valet_feature');
-}
-
export function registerOAuthApp(app, success, error) {
$.ajax({
url: '/api/v1/oauth/register',
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 41e9e9ca6..75e80bc7e 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -35,8 +35,8 @@ module.exports = {
RECIEVED_TEAM: null,
RECIEVED_CONFIG: null,
-
RECIEVED_LOGS: null,
+ RECIEVED_ALL_TEAMS: null,
TOGGLE_IMPORT_THEME_MODAL: null
}),
@@ -72,6 +72,10 @@ module.exports = {
MAX_FILE_SIZE: 50000000, // 50 MB
THUMBNAIL_WIDTH: 128,
THUMBNAIL_HEIGHT: 100,
+ WEB_VIDEO_WIDTH: 640,
+ WEB_VIDEO_HEIGHT: 480,
+ MOBILE_VIDEO_WIDTH: 480,
+ MOBILE_VIDEO_HEIGHT: 360,
DEFAULT_CHANNEL: 'town-square',
OFFTOPIC_CHANNEL: 'off-topic',
GITLAB_SERVICE: 'gitlab',
@@ -130,32 +134,34 @@ module.exports = {
mentionColor: '#ffffff',
centerChannelBg: '#ffffff',
centerChannelColor: '#333333',
+ newMessageSeparator: '#FF8800',
linkColor: '#2389d7',
buttonBg: '#2389d7',
buttonColor: '#FFFFFF'
},
- slack: {
- type: 'Slack',
- sidebarBg: '#4D394B',
- sidebarText: '#ab9ba9',
- sidebarUnreadText: '#FFFFFF',
- sidebarTextHoverBg: '#3e313c',
- sidebarTextHoverColor: '#ab9ba9',
- sidebarTextActiveBg: '#4c9689',
+ organization: {
+ type: 'Organization',
+ sidebarBg: '#2071a7',
+ sidebarText: '#bfcde8',
+ sidebarUnreadText: '#fff',
+ sidebarTextHoverBg: '#136197',
+ sidebarTextHoverColor: '#bfcde8',
+ sidebarTextActiveBg: '#136197',
sidebarTextActiveColor: '#FFFFFF',
- sidebarHeaderBg: '#4D394B',
+ sidebarHeaderBg: '#2f81b7',
sidebarHeaderTextColor: '#FFFFFF',
- onlineIndicator: '#4c9689',
- mentionBj: '#eb4d5c',
- mentionColor: '#FFFFFF',
- centerChannelBg: '#FFFFFF',
+ onlineIndicator: '#7DBE00',
+ mentionBj: '#136197',
+ mentionColor: '#bfcde8',
+ centerChannelBg: '#f2f4f8',
centerChannelColor: '#333333',
- linkColor: '#2389d7',
- buttonBg: '#26a970',
+ newMessageSeparator: '#FF8800',
+ linkColor: '#2f81b7',
+ buttonBg: '#1dacfc',
buttonColor: '#FFFFFF'
},
- dark: {
- type: 'Dark',
+ mattermostDark: {
+ type: 'Mattermost Dark',
sidebarBg: '#1B2C3E',
sidebarText: '#bbbbbb',
sidebarUnreadText: '#fff',
@@ -165,13 +171,35 @@ module.exports = {
sidebarTextActiveColor: '#FFFFFF',
sidebarHeaderBg: '#1B2C3E',
sidebarHeaderTextColor: '#FFFFFF',
- onlineIndicator: '#4c9689',
+ onlineIndicator: '#55C5B2',
mentionBj: '#B74A4A',
mentionColor: '#FFFFFF',
centerChannelBg: '#2F3E4E',
centerChannelColor: '#DDDDDD',
+ newMessageSeparator: '#5de5da',
linkColor: '#A4FFEB',
- buttonBg: '#2B9C99',
+ buttonBg: '#1dacfc',
+ buttonColor: '#FFFFFF'
+ },
+ windows10: {
+ type: 'Windows Dark',
+ sidebarBg: '#171717',
+ sidebarText: '#eee',
+ sidebarUnreadText: '#fff',
+ sidebarTextHoverBg: '#302e30',
+ sidebarTextHoverColor: '#fff',
+ sidebarTextActiveBg: '#484748',
+ sidebarTextActiveColor: '#FFFFFF',
+ sidebarHeaderBg: '#1f1f1f',
+ sidebarHeaderTextColor: '#FFFFFF',
+ onlineIndicator: '#0177e7',
+ mentionBj: '#0177e7',
+ mentionColor: '#FFFFFF',
+ centerChannelBg: '#1F1F1F',
+ centerChannelColor: '#DDDDDD',
+ newMessageSeparator: '#CC992D',
+ linkColor: '#0177e7',
+ buttonBg: '#0177e7',
buttonColor: '#FFFFFF'
}
},
@@ -182,7 +210,7 @@ module.exports = {
},
{
id: 'sidebarText',
- uiName: 'Sidebar text color'
+ uiName: 'Sidebar Text'
},
{
id: 'sidebarHeaderBg',
@@ -190,51 +218,55 @@ module.exports = {
},
{
id: 'sidebarHeaderTextColor',
- uiName: 'Sidebar Header text color'
+ uiName: 'Sidebar Header Text'
},
{
id: 'sidebarUnreadText',
- uiName: 'Sidebar unread text color'
+ uiName: 'Sidebar Unread Text'
},
{
id: 'sidebarTextHoverBg',
- uiName: 'Sidebar text hover BG'
+ uiName: 'Sidebar Text Hover BG'
},
{
id: 'sidebarTextHoverColor',
- uiName: 'Sidebar text hover color'
+ uiName: 'Sidebar Text Hover Color'
},
{
id: 'sidebarTextActiveBg',
- uiName: 'Sidebar text active BG'
+ uiName: 'Sidebar Text Active BG'
},
{
id: 'sidebarTextActiveColor',
- uiName: 'Sidebar text active color'
+ uiName: 'Sidebar Text Active Color'
},
{
id: 'onlineIndicator',
- uiName: 'Online indicator'
+ uiName: 'Online Indicator'
},
{
id: 'mentionBj',
- uiName: 'Mention jewel BG'
+ uiName: 'Mention Jewel BG'
},
{
id: 'mentionColor',
- uiName: 'Mention jewel text color'
+ uiName: 'Mention Jewel Text'
},
{
id: 'centerChannelBg',
- uiName: 'Center channel BG'
+ uiName: 'Center Channel BG'
},
{
id: 'centerChannelColor',
- uiName: 'Center channel text color'
+ uiName: 'Center Channel Text'
+ },
+ {
+ id: 'newMessageSeparator',
+ uiName: 'New message separator'
},
{
id: 'linkColor',
- uiName: 'Link color'
+ uiName: 'Link Color'
},
{
id: 'buttonBg',
@@ -243,7 +275,7 @@ module.exports = {
{
id: 'buttonColor',
- uiName: 'Button Color'
+ uiName: 'Button Text'
}
]
};
diff --git a/web/react/utils/emoticons.jsx b/web/react/utils/emoticons.jsx
index 7210201ff..a7c837199 100644
--- a/web/react/utils/emoticons.jsx
+++ b/web/react/utils/emoticons.jsx
@@ -5,15 +5,14 @@ const emoticonPatterns = {
smile: /:-?\)/g, // :)
open_mouth: /:o/gi, // :o
scream: /:-o/gi, // :-o
- smirk: /[:;]-?]/g, // :]
- grinning: /[:;]-?d/gi, // :D
+ smirk: /:-?]/g, // :]
+ grinning: /:-?d/gi, // :D
stuck_out_tongue_closed_eyes: /x-d/gi, // x-d
- stuck_out_tongue_winking_eye: /[:;]-?p/gi, // ;p
+ stuck_out_tongue_winking_eye: /:-?p/gi, // :p
rage: /:-?[\[@]/g, // :@
frowning: /:-?\(/g, // :(
sob: /:['’]-?\(|:&#x27;\(/g, // :`(
kissing_heart: /:-?\*/g, // :*
- wink: /;-?\)/g, // ;)
pensive: /:-?\//g, // :/
confounded: /:-?s/gi, // :s
flushed: /:-?\|/g, // :|
diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx
index c6ffb1871..7e88f8644 100644
--- a/web/react/utils/markdown.jsx
+++ b/web/react/utils/markdown.jsx
@@ -15,6 +15,14 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
this.formattingOptions = formattingOptions;
}
+ br() {
+ if (this.formattingOptions.singleline) {
+ return ' ';
+ }
+
+ return super.br();
+ }
+
heading(text, level, raw) {
const id = `${this.options.headerPrefix}${raw.toLowerCase().replace(/[^\w]+/g, '-')}`;
return `<h${level} id="${id}" class="markdown__heading">${text}</h${level}>`;
@@ -36,6 +44,14 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
return output;
}
+ paragraph(text) {
+ if (this.formattingOptions.singleline) {
+ return `<p class="markdown__paragraph-inline">${text}</p>`;
+ }
+
+ return super.paragraph(text);
+ }
+
table(header, body) {
return `<table class="markdown__table"><thead>${header}</thead><tbody>${body}</tbody></table>`;
}
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 82bb82d6b..61dcae6d8 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -4,6 +4,7 @@
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var ChannelStore = require('../stores/channel_store.jsx');
var UserStore = require('../stores/user_store.jsx');
+var TeamStore = require('../stores/team_store.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
var AsyncClient = require('./async_client.jsx');
@@ -113,7 +114,7 @@ export function notifyMe(title, body, channel) {
if (channel) {
switchChannel(channel);
} else {
- window.location.href = '/';
+ window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square';
}
};
setTimeout(function closeNotificationOnTimeout() {
@@ -314,7 +315,6 @@ function getYoutubeEmbed(link) {
$('.video-type.' + youtubeId).html('Youtube - ');
$('.video-uploader.' + youtubeId).html(metadata.channelTitle);
$('.video-title.' + youtubeId).find('a').html(metadata.title);
- $('.post-list-holder-by-time').scrollTop($('.post-list-holder-by-time')[0].scrollHeight);
}
if (global.window.config.GoogleDeveloperKey) {
@@ -553,7 +553,6 @@ export function applyTheme(theme) {
changeCss('.sidebar--left .nav li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.8), 1);
changeCss('.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + theme.sidebarText, 1);
changeCss('.sidebar--left, .sidebar--right .sidebar--right__header', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 1);
- changeCss('.sidebar--right .sidebar-right__body', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 2);
changeCss('.sidebar--left .status path', 'fill:' + changeOpacity(theme.sidebarText, 0.5), 1);
}
@@ -607,29 +606,37 @@ export function applyTheme(theme) {
}
if (theme.centerChannelBg) {
- changeCss('.app__content', 'background:' + theme.centerChannelBg, 1);
+ changeCss('.app__content, .markdown__table, .markdown__table tbody tr', 'background:' + theme.centerChannelBg, 1);
changeCss('#post-list .post-list-holder-by-time', 'background:' + theme.centerChannelBg, 1);
changeCss('#post-create', 'background:' + theme.centerChannelBg, 1);
changeCss('.search-bar__container .search__form .search-bar', 'background:' + theme.centerChannelBg, 1);
changeCss('.date-separator .separator__text, .new-separator .separator__text', 'background:' + theme.centerChannelBg, 1);
+ changeCss('.post-image__column .post-image__details', 'background:' + theme.centerChannelBg, 1);
changeCss('.sidebar--right', 'background:' + theme.centerChannelBg, 1);
}
if (theme.centerChannelColor) {
- changeCss('.app__content', 'color:' + theme.centerChannelColor, 2);
+ changeCss('.app__content, .post-create__container .post-create-body .btn-file, .post-create__container .post-create-footer .msg-typing, .loading-screen .loading__content .round', 'color:' + theme.centerChannelColor, 1);
changeCss('#post-create', 'color:' + theme.centerChannelColor, 2);
+ changeCss('.post-body hr', 'background:' + theme.centerChannelColor, 1);
changeCss('.channel-header .heading', 'color:' + theme.centerChannelColor, 1);
+ changeCss('.markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
changeCss('.channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
- changeCss('.custom-textarea', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2) + '!important; color: ' + theme.centerChannelColor, 1);
+ changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.custom-textarea', 'color:' + theme.centerChannelColor, 1);
+ changeCss('.post-image__column', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 2);
+ changeCss('.post-image__column .post-image__details', 'color:' + theme.centerChannelColor, 2);
+ changeCss('.post-image__column a, .post-image__column a:hover, .post-image__column a:focus', 'color:' + theme.centerChannelColor, 1);
changeCss('.search-bar__container .search__form .search-bar', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: ' + theme.centerChannelColor, 2);
changeCss('.search-bar__container .search__form', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.channel-intro .channel-intro__content', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1);
- changeCss('.date-separator .separator__text, .new-separator .separator__text', 'color:' + theme.centerChannelColor, 2);
- changeCss('.date-separator .separator__hr, .new-separator .separator__hr, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.date-separator .separator__text', 'color:' + theme.centerChannelColor, 2);
+ changeCss('.date-separator .separator__hr, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.channel-intro', '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', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
+ 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', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 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);
changeCss('@media(max-width: 1800px){.inner__wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
changeCss('.post:hover, .sidebar--right .sidebar--right__header', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
@@ -639,6 +646,11 @@ export function applyTheme(theme) {
changeCss('.sidebar--right', 'color:' + theme.centerChannelColor, 2);
}
+ if (theme.newMessageSeparator) {
+ changeCss('.new-separator .separator__text', 'color:' + theme.newMessageSeparator, 1);
+ changeCss('.new-separator .separator__hr', 'border-color:' + changeOpacity(theme.newMessageSeparator, 0.5), 1);
+ }
+
if (theme.linkColor) {
changeCss('a, a:focus, a:hover', 'color:' + theme.linkColor, 1);
changeCss('.post .comment-icon__container', 'fill:' + theme.linkColor, 1);
diff --git a/web/sass-files/sass/partials/_admin-console.scss b/web/sass-files/sass/partials/_admin-console.scss
index 9823d2611..11794a269 100644
--- a/web/sass-files/sass/partials/_admin-console.scss
+++ b/web/sass-files/sass/partials/_admin-console.scss
@@ -1,5 +1,5 @@
-.sidebar--left {
- &.sidebar--collapsable {
+
+.sidebar--left { &.sidebar--collapsable {
background: #333;
.team__header {
background: transparent;
@@ -9,7 +9,7 @@
li {
padding: 0;
.icon {
- width: 15px;
+ width: 17px;
}
> a {
color: #fff;
@@ -19,6 +19,11 @@
background-color: $primary-color;
}
}
+ > h4 {
+ background: #333;
+ padding: 10px 10px;
+ margin: 1px 0 0;
+ }
}
.menu-icon--right {
vertical-align: top;
@@ -29,12 +34,15 @@
font-size: 13px;
right: -2px;
position: relative;
+ color: #fff;
}
}
&.nav__sub-menu {
- padding: 5px 0;
background: #111;
-webkit-font-smoothing: auto;
+ &.padded {
+ padding: 5px 0;
+ }
li {
> a {
font-size: 13px;
@@ -88,6 +96,7 @@
overflow: auto;
background-color: #f1f1f1;
padding: 0 20px 20px;
+ min-height: 600px;
}
.wrapper--fixed {
max-width: 800px;
diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss
index 592d5e62e..87d9b8200 100644
--- a/web/sass-files/sass/partials/_base.scss
+++ b/web/sass-files/sass/partials/_base.scss
@@ -34,8 +34,18 @@ body {
}
}
-b, strong {
- font-weight: 600;
+.popover {
+ color: #333;
+ a {
+ color: $primary-color;
+ &:hover, &:focus {
+ color: $primary-color;
+ }
+ }
+}
+
+.word-break--all {
+ word-break: break-all;
}
a {
diff --git a/web/sass-files/sass/partials/_files.scss b/web/sass-files/sass/partials/_files.scss
index 405265f92..6dbb82810 100644
--- a/web/sass-files/sass/partials/_files.scss
+++ b/web/sass-files/sass/partials/_files.scss
@@ -157,23 +157,23 @@
border-left: 1px solid #ddd;
font-size: 13px;
padding: 7px;
+ color: #333;
.post-image__name {
margin-bottom: 3px;
display: block;
- color: #333;
}
.post-image__download {
display: inline-block;
padding-right: 7px;
cursor: pointer;
- color: #555;
+ @include opacity(0.7);
}
.post-image__type {
- color: grey;
+ @include opacity(0.6);
}
.post-image__size {
margin-left: 4px;
- color: grey;
+ @include opacity(0.6);
}
}
a {
@@ -214,4 +214,3 @@
}
}
}
-
diff --git a/web/sass-files/sass/partials/_loading.scss b/web/sass-files/sass/partials/_loading.scss
index d71055722..ab8601df6 100644
--- a/web/sass-files/sass/partials/_loading.scss
+++ b/web/sass-files/sass/partials/_loading.scss
@@ -37,4 +37,4 @@
to { opacity: 0.1; }
}
}
-}
+} \ No newline at end of file
diff --git a/web/sass-files/sass/partials/_markdown.scss b/web/sass-files/sass/partials/_markdown.scss
index c09e9d7b4..122586354 100644
--- a/web/sass-files/sass/partials/_markdown.scss
+++ b/web/sass-files/sass/partials/_markdown.scss
@@ -1,5 +1,21 @@
.markdown__heading {
- font-weight: bold;
+ font-weight: 700;
+}
+.markdown__paragraph-inline {
+ display: inline;
+ + .markdown__paragraph-inline {
+ margin-left: 4px;
+ }
+}
+.post-body {
+ hr {
+ height: 4px;
+ padding: 0;
+ margin: 15px 0 16px;
+ background-color: #e7e7e7;
+ border: 0 none;
+ @include opacity(0.2);
+ }
}
.markdown__table {
background: #fff;
@@ -15,6 +31,26 @@
}
}
}
+blockquote {
+ border: none;
+ position: relative;
+ font-size: 16px;
+ padding: 10px 10px 10px 38px;
+ margin-bottom: 0;
+ &:before {
+ font-family: FontAwesome;
+ font-weight: normal;
+ font-style: normal;
+ display: inline-block;
+ text-decoration: inherit;
+ content: "\f10d";
+ left: 8px;
+ top: 5px;
+ position: absolute;
+ font-size: 20px;
+ @include opacity(0.6);
+ }
+}
pre {
border: none;
background-color: #f7f7f7;
@@ -25,4 +61,7 @@ pre {
code {
color: #c7254e;
}
-} \ No newline at end of file
+}
+code {
+ background: #fff;
+}
diff --git a/web/sass-files/sass/partials/_mentions.scss b/web/sass-files/sass/partials/_mentions.scss
index aa893c535..48746ba01 100644
--- a/web/sass-files/sass/partials/_mentions.scss
+++ b/web/sass-files/sass/partials/_mentions.scss
@@ -63,10 +63,11 @@
padding-left: 10px;
}
-.mention-highlight {
- background-color:#fff2bb;
+.mention-link {
+ color: $primary-color;
}
-.mention-link {
- color:$primary-color;
+.mention-highlight {
+ background-color:#fff2bb;
+ color: #333;
} \ No newline at end of file
diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss
index 38e9b4174..e4e8b20b6 100644
--- a/web/sass-files/sass/partials/_modal.scss
+++ b/web/sass-files/sass/partials/_modal.scss
@@ -3,6 +3,7 @@
}
.modal {
width: 100%;
+ color: #333;
&.image_modal {
.modal-backdrop.in {
@include opacity(0.7);
@@ -10,12 +11,26 @@
}
a, a:focus, a:hover {
color: #2389D7;
+ &.text-danger {
+ color: #a94442;
+ }
}
- .btn.btn-primary {
- background: #4285f4;
- &:hover, &:focus, &:active {
- background: $primary-color--hover;
- color: #fff;
+ .custom-textarea {
+ color: inherit;
+ border-color: #ccc;
+ &:focus {
+ border-color: #ccc;
+ box-shadow: none;
+ }
+ }
+ .btn {
+ font-size: 13px;
+ &.btn-primary {
+ background: #4285f4;
+ &:hover, &:focus, &:active {
+ background: $primary-color--hover;
+ color: #fff;
+ }
}
}
.info__label {
@@ -181,16 +196,13 @@
position: relative;
max-width: 90%;
min-height: 100px;
- min-width: 320px;
+ min-width: 240px;
@include border-radius(3px);
display: table;
margin: 0 auto;
&:hover {
@include border-radius(3px 3px 0 0);
}
- &:hover .modal-close {
- @include opacity(1);
- }
&.default {
width: 100%;
height: 80%;
@@ -204,8 +216,15 @@
right: -13px;
top: -13px;
@include opacity(0);
+ -webkit-transition: opacity 0.6s;
+ -moz-transition: opacity 0.6s;
+ -o-transition: opacity 0.6s;
+ transition: opacity 0.6s;
cursor: pointer;
z-index: 9999;
+ &.modal-close--show {
+ @include opacity(1);
+ }
}
> a {
background: #FFF;
@@ -217,7 +236,7 @@
max-height: 100%;
}
}
- .image-content {
+ .modal-content{
box-shadow: none;
background: rgba(0, 0, 0, 0);
width: 100%;
@@ -301,6 +320,7 @@
}
}
+
// Invite New Member
.invite {
margin-right: 40px;
@@ -315,4 +335,4 @@
padding-left: 0;
}
}
-} \ No newline at end of file
+}
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index d4f02cf4b..7532875d6 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -2,13 +2,17 @@
white-space:pre-wrap;
word-wrap:break-word;
background:transparent;
- border:1px solid #ccc !important;
+ border:1px solid #ccc;
position:absolute;
top:0px;
height:auto;
resize: none;
line-height:20px;
min-height:36px;
+ &:focus {
+ border-color: #ccc;
+ box-shadow: none;
+ }
}
.bad-connection {
@@ -225,17 +229,17 @@ body.ios {
right: 0;
position: absolute;
top: 1px;
- color: #999;
+ color: #444;
+ @include opacity(0.5);
@include single-transition(all, 0.15s);
font-size: 16px;
padding: 7px 9px 6px;
&:hover, &:active {
- color: #444;
+ @include opacity(0.9);
box-shadow: none;
}
}
textarea {
- border: none;
box-shadow: none;
}
}
@@ -253,7 +257,7 @@ body.ios {
line-height: 18px;
display: inline-block;
font-size: 13px;
- color: #777;
+ @include opacity(0.6);
}
}
}
diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss
index 8dcd8f35c..3aab05d70 100644
--- a/web/sass-files/sass/partials/_settings.scss
+++ b/web/sass-files/sass/partials/_settings.scss
@@ -32,6 +32,7 @@
}
.settings-table {
display: table;
+ table-layout: fixed;
width: 100%;
> div {
display: table-cell;
@@ -125,6 +126,10 @@
}
}
+ .font--small {
+ font-size: 13px;
+ }
+
.section-describe {
color:grey;
}
@@ -155,8 +160,18 @@
.has-error {
color: #a94442;
}
+ .padding-top {
+ padding-top: 7px;
+ &.x2 {
+ padding-top: 14px;
+ }
+ }
.control-label {
color: #555;
+ font-weight: 600;
+ &.text-left {
+ text-align: left;
+ }
}
hr {
border-color: #ccc;
diff --git a/web/sass-files/sass/partials/_videos.scss b/web/sass-files/sass/partials/_videos.scss
index de18aa08a..9e1ce29b7 100644
--- a/web/sass-files/sass/partials/_videos.scss
+++ b/web/sass-files/sass/partials/_videos.scss
@@ -1,6 +1,7 @@
.video-div {
position:relative;
max-width: 480px;
+ margin-bottom: 8px;
.video-thumbnail {
max-width: 100%;
height: auto;
@@ -19,18 +20,15 @@
}
.video-type {
- color:grey;
+ @include opacity(0.8);
font-size:15px;
- font-weight:200;
margin:0px;
padding:0px;
}
.video-uploader {
- font-size:15px;
- margin-top:3px;
- margin-bottom:0px;
- padding:0px;
+ font-size: 13px;
+ margin: 0 0 15px;
}
.video-title {
diff --git a/web/static/images/themes/dark.png b/web/static/images/themes/mattermost dark.png
index 832f64d2e..832f64d2e 100644
--- a/web/static/images/themes/dark.png
+++ b/web/static/images/themes/mattermost dark.png
Binary files differ
diff --git a/web/static/images/themes/organization.png b/web/static/images/themes/organization.png
new file mode 100644
index 000000000..1a38bfb34
--- /dev/null
+++ b/web/static/images/themes/organization.png
Binary files differ
diff --git a/web/static/images/themes/windows dark.png b/web/static/images/themes/windows dark.png
new file mode 100644
index 000000000..d65304820
--- /dev/null
+++ b/web/static/images/themes/windows dark.png
Binary files differ
diff --git a/web/web.go b/web/web.go
index 3b36f3d56..da7eff13d 100644
--- a/web/web.go
+++ b/web/web.go
@@ -175,8 +175,7 @@ func login(c *api.Context, w http.ResponseWriter, r *http.Request) {
var team *model.Team
if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil {
l4g.Error("Couldn't find team name=%v, teamURL=%v, err=%v", teamName, c.GetTeamURL(), tResult.Err.Message)
- // This should probably do somthing nicer
- http.Redirect(w, r, "http://"+r.Host, http.StatusTemporaryRedirect)
+ http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
return
} else {
team = tResult.Data.(*model.Team)