summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml20
-rw-r--r--CHANGELOG.md4
-rw-r--r--Makefile5
-rw-r--r--README.md10
-rw-r--r--api/channel.go2
-rw-r--r--api/context.go24
-rw-r--r--api/user.go39
-rw-r--r--config/config.json7
-rw-r--r--doc/README.md37
-rw-r--r--doc/developer/Code-Contribution-Guidelines.md (renamed from doc/developer/code-contribution.md)2
-rw-r--r--doc/developer/Setup.md (renamed from doc/install/dev-setup.md)0
-rw-r--r--doc/developer/Style-Guide.md (renamed from doc/developer/style-guide.md)0
-rw-r--r--doc/favicon.ico0
-rw-r--r--doc/install/Amazon-Elastic-Beanstalk.md (renamed from doc/install/aws-ebs-setup.md)6
-rw-r--r--doc/install/Docker-Single-Container.md (renamed from doc/install/single-container-install.md)4
-rw-r--r--doc/install/Production-Ubuntu.md (renamed from doc/install/prod-ubuntu.md)0
-rw-r--r--doc/install/Release-Numbering.md (renamed from doc/install/release-numbering.md)0
-rw-r--r--doc/install/Requirements.md (renamed from doc/install/requirements.md)0
-rw-r--r--doc/install/SMTP-Email-Setup.md (renamed from doc/config/smtp-email-setup.md)0
-rw-r--r--doc/integrations/Single-Sign-On/Gitlab.md (renamed from doc/integrations/sso/gitlab-sso.md)2
-rw-r--r--doc/integrations/webhooks/Incoming.md (renamed from doc/integrations/webhook/incoming.md)2
-rw-r--r--doc/usage/Markdown.md (renamed from doc/help/enduser/markdown.md)0
-rw-r--r--doc/usage/Slack-Import.md (renamed from doc/import/slack-import.md)0
-rw-r--r--docker/1.0/Dockerfile2
-rw-r--r--docker/dev/config_docker.json5
-rw-r--r--docker/local/config_docker.json5
-rw-r--r--mattermost.go51
-rw-r--r--mkdocs.yml11
-rw-r--r--model/config.go3
-rw-r--r--model/session.go1
-rw-r--r--model/utils.go1
-rw-r--r--model/version.go4
-rw-r--r--utils/config.go2
-rw-r--r--utils/diagnostic.go45
-rw-r--r--web/react/.eslintrc19
-rw-r--r--web/react/components/access_history_modal.jsx24
-rw-r--r--web/react/components/activity_log_modal.jsx24
-rw-r--r--web/react/components/admin_console/privacy_settings.jsx34
-rw-r--r--web/react/components/admin_console/service_settings.jsx70
-rw-r--r--web/react/components/channel_header.jsx40
-rw-r--r--web/react/components/channel_members.jsx16
-rw-r--r--web/react/components/channel_notifications.jsx35
-rw-r--r--web/react/components/command_list.jsx2
-rw-r--r--web/react/components/create_comment.jsx12
-rw-r--r--web/react/components/create_post.jsx12
-rw-r--r--web/react/components/delete_channel_modal.jsx16
-rw-r--r--web/react/components/delete_post_modal.jsx24
-rw-r--r--web/react/components/edit_channel_modal.jsx10
-rw-r--r--web/react/components/file_upload.jsx2
-rw-r--r--web/react/components/get_link_modal.jsx18
-rw-r--r--web/react/components/login.jsx6
-rw-r--r--web/react/components/mention.jsx6
-rw-r--r--web/react/components/mention_list.jsx91
-rw-r--r--web/react/components/more_channels.jsx98
-rw-r--r--web/react/components/more_direct_channels.jsx32
-rw-r--r--web/react/components/navbar_dropdown.jsx12
-rw-r--r--web/react/components/popover_list_members.jsx3
-rw-r--r--web/react/components/post.jsx9
-rw-r--r--web/react/components/post_body.jsx2
-rw-r--r--web/react/components/post_header.jsx20
-rw-r--r--web/react/components/post_list.jsx29
-rw-r--r--web/react/components/rename_channel_modal.jsx20
-rw-r--r--web/react/components/sidebar.jsx36
-rw-r--r--web/react/components/sidebar_header.jsx3
-rw-r--r--web/react/components/sidebar_right.jsx6
-rw-r--r--web/react/components/signup_user_complete.jsx27
-rw-r--r--web/react/components/team_signup_email_item.jsx9
-rw-r--r--web/react/components/team_signup_send_invites_page.jsx6
-rw-r--r--web/react/components/team_signup_url_page.jsx30
-rw-r--r--web/react/components/team_signup_welcome_page.jsx3
-rw-r--r--web/react/components/user_profile.jsx16
-rw-r--r--web/react/components/user_settings/import_theme_modal.jsx4
-rw-r--r--web/react/components/user_settings/premade_theme_chooser.jsx2
-rw-r--r--web/react/package.json4
-rw-r--r--web/react/pages/authorize.jsx16
-rw-r--r--web/react/pages/channel.jsx4
-rw-r--r--web/react/stores/browser_store.jsx2
-rw-r--r--web/react/stores/team_store.jsx8
-rw-r--r--web/react/utils/client.jsx2
-rw-r--r--web/react/utils/constants.jsx2
-rw-r--r--web/react/utils/utils.jsx60
-rw-r--r--web/sass-files/sass/partials/_admin-console.scss1
-rw-r--r--web/sass-files/sass/partials/_base.scss47
-rw-r--r--web/sass-files/sass/partials/_forms.scss5
-rw-r--r--web/sass-files/sass/partials/_headers.scss20
-rw-r--r--web/sass-files/sass/partials/_mentions.scss12
-rw-r--r--web/sass-files/sass/partials/_modal.scss17
-rw-r--r--web/sass-files/sass/partials/_oauth.scss31
-rw-r--r--web/sass-files/sass/partials/_post.scss8
-rw-r--r--web/sass-files/sass/partials/_responsive.scss6
-rw-r--r--web/sass-files/sass/partials/_settings.scss12
-rw-r--r--web/sass-files/sass/partials/_sidebar--left.scss13
-rw-r--r--web/sass-files/sass/partials/_sidebar--menu.scss2
-rw-r--r--web/sass-files/sass/styles.scss3
-rw-r--r--web/static/images/themes/mattermost dark.pngbin75371 -> 104882 bytes
-rw-r--r--web/static/images/themes/mattermost.pngbin66828 -> 93862 bytes
-rw-r--r--web/static/images/themes/organization.pngbin86044 -> 127558 bytes
-rw-r--r--web/static/images/themes/slack.pngbin68603 -> 0 bytes
-rw-r--r--web/static/images/themes/windows dark.pngbin82784 -> 122145 bytes
-rw-r--r--web/templates/authorize.html40
-rw-r--r--web/web.go81
101 files changed, 997 insertions, 521 deletions
diff --git a/.travis.yml b/.travis.yml
index 02e1234d3..1af940d21 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -20,6 +20,7 @@ before_install:
- sudo sed -i'' 's/log_slow_queries/slow_query_log/' /etc/mysql/my.cnf
- sudo sed -i'' 's/basedir[^=]\+=.*$/basedir = \/opt\/mysql\/server-5.6/' /etc/mysql/my.cnf
- sudo /etc/init.d/mysql.server start
+- sudo pip install mkdocs
install:
- export PATH=$PATH:$HOME/gopath/bin
- go get github.com/tools/godep
@@ -37,6 +38,7 @@ addons:
hosts:
- 127.0.0.1 dockerhost
deploy:
+# Github releases, builds only on tags
- provider: releases
api_key:
secure: ma8Y0oimU+LB6LTAh8to2E1/ghaDPhcsAFXBrODsHpd4JgxA6HYoEwSEBCJFHSpu/JteclsxSTfp9hcuzw/IOtlwlSAiVoBZ60s24MRKTIAQNtrJ4QrX5wyfAZi+Bcuk/E8NynmoIW5qpaElSAdjgocyjAJIQ5ChMEztglL0cAEBXQRWbWMqSZ0hVLPrKDCIkWIyv3pFxqdLOxktkzxW07r2dlT0hppXR3dCaPJo0nelArS2H3LdN/3Iv6cAddfS27RaZkqDj/PDh6OZr4EguC99TxlVNChIr7nPr3/OiAssbkvEnhlSLeABFO9+7KfutL2WhAjpFXTjtPVq6Qalc8UW0K0gxq//sVfhb1MzjenmdOf06uB2bilQ8kgwHo7dDdRZBqqAtxQ6Q0Ht3SFMj6v/1zVD3s+YX/kWCEbUTHm6r2G/eF794ozcJyU+6j1L8hm6mvf8Mr9XCqBfgpZy6FCLX+9OKdMvX2jY8reo3Xz1PA9R6yzhN08vjku+jW+fsoYrBLd0fY1UGK2uOuvBByCeJzXupd3YpBMjEyRupVxqEj7K0GWOJeml65mkqKSNsHdDSeSjMpb8mwneZyTbdjsxCFQRLcLgpAajFrkk4G2Yz3KfhXSo29XKEGX+EbY5NuP8KmDsBsguPI0zfwv/co0hAY8PIIcehxcdoR9Vb2c=
@@ -48,6 +50,7 @@ deploy:
go: 1.4.2
condition: $TRAVIS_DB = mysql
+# S3 deploy for master docker image. (latest compiled bits)
- provider: s3
access_key_id: AKIAJCO3KJYEGWJIKDIQ
secret_access_key:
@@ -63,3 +66,20 @@ deploy:
branch: master
go: 1.4.2
condition: $TRAVIS_DB = mysql
+
+# S3 deploy for documentation
+ - provider: s3
+ access_key_id: AKIAJCO3KJYEGWJIKDIQ
+ secret_access_key:
+ secure: p66X2tJBmKgtcVyPtGgkAwW29IiRojqGA39RjCJkIWNTJ0e/9JvBOiMS2c4a7I4aOads38rsthwdaigBWagDWNH7bGsEZN7B0TszZuFAuU+XGjU5A66MIOfFfzbUg8AnByysr+XG5/bknFIrP/XhM2fbRr6gbYrFUK7TNkpgjFs5u3BzUrz2iTAV8uOpSJqKSnaf0pTZk1EywOK/X8W8ViIjc7Di3FzQcqIW9K3D27N+3rVsv8SRT1hWASVlnG6aThqqebiM8FCGCzAYVgQb3h3Wu8JT5fIz7Qo7A6siVRwNBwWwzP8HkGoinEK32Wsj/fDXk27vjpFQO/+9sV0xfcTbIZA6MnuYWF4rHOT59KcshCWCD3V0FopX57p/dtOzM9+6lxIctAT++izxWoZit/5c5A4633iY1d+RMeTko1POix6MSlxPMRHZUFwSXROgFuWWRpyD6TlUTCST9/wTTd0WDPklAAiYcnuEPW3qCnw0r0xkrA4AwWUXqXdAIwDt5bA27KcjRyY4Fofv9NxH09BNuBTXNPrvnYPZMmaKrv+HOX3NFTreuV6+5LJdhYUxYSBvSWo1jeWIQ5Q9RUdTU0PqmKpMhJKbKey/S4gxCXHg2HR8DwLCcbIZcvneF9yPEAT71YA6zpLKoPVSwWwH97huKSzjpic/RUfFXQOcgCQ=
+ bucket: docs.mattermost.org
+ local_dir: documentation-html
+ acl: public_read
+ region: us-east-1
+ skip_cleanup: true
+ detect_encoding: true
+ on:
+ repo: mattermost/platform
+ branch: master
+ go: 1.4.2
+ condition: $TRAVIS_DB = mysql
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7a4335eeb..7357796e2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,7 +35,9 @@ Added new web-based System Console for managing instance level configuration. Th
- _monitor operations_, by quickly accessing log files and user roles, and
- _manage teams_, with essential functions such as team role assignment and password reset
-In addition new command line tools are available for managing Mattermost system roles, creating users, resetting passwords, getting version info and other basic tasks.
+In addition new command line tools are available for managing Mattermost system roles, creating users, resetting passwords, getting version info and other basic tasks.
+
+Run `./platform -h` for documentation using the new command line tool.
### New Features
diff --git a/Makefile b/Makefile
index 88df59d8b..8d6d30735 100644
--- a/Makefile
+++ b/Makefile
@@ -106,7 +106,12 @@ travis:
@sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
rm $(DIST_PATH)/web/templates/*.bak
+ mv doc/README.md doc/index.md
+ mkdocs build
+ cp -r documentation-html $(DIST_PATH)/documentation-html
+
tar -C dist -czf $(DIST_PATH).tar.gz mattermost
+ rm -r $(DIST_PATH)
build:
@$(GO) build $(GOFLAGS) ./...
diff --git a/README.md b/README.md
index 88f6dcb35..08f01f717 100644
--- a/README.md
+++ b/README.md
@@ -42,19 +42,19 @@ There are multiple ways to install Mattermost depending on your needs.
#### Quick Start Install for Product Evaluation
-- [Local Machine Install with Docker](doc/install/single-container-install.md) - Explore product functionality using a single-container Docker install on a local machine, including Mac OSX, Ubuntu, or Arch Linux). Optionally set up email and upgrade your instance using DockerHub.
+- [Local Machine Install with Docker](http://docs.mattermost.org/install/Docker-Single-Container/index.html) - Explore product functionality using a single-container Docker install on a local machine, including Mac OSX, Ubuntu, or Arch Linux). Optionally set up email and upgrade your instance using DockerHub.
-- [AWS EBS Install with Docker](doc/install/aws-ebs-setup.md) - Explore product functionality using a single-container Docker install for Amazon Web Services Elastic Beanstalk. Optionally set up email and upgrade your instance using DockerHub.
+- [AWS EBS Install with Docker](http://docs.mattermost.org/install/Amazon-Elastic-Beanstalk/index.html) - Explore product functionality using a single-container Docker install for Amazon Web Services Elastic Beanstalk. Optionally set up email and upgrade your instance using DockerHub.
#### Development Install
-- [Developer Machine Setup](doc/install/dev-setup.md) - Setup your local machine development environment using Docker on Mac OSX or Ubuntu. Pull the latest stable release or pull the latest code from our development build.
+- [Developer Machine Setup](http://docs.mattermost.org/developer/Setup/index.html) - Setup your local machine development environment using Docker on Mac OSX or Ubuntu. Pull the latest stable release or pull the latest code from our development build.
[![Build Status](https://travis-ci.org/mattermost/platform.svg?branch=master)](https://travis-ci.org/mattermost/platform)
#### Production Deployment
-Prior to production installation, please review [Mattermost system requirements](doc/install/requirements.md).
+Prior to production installation, please review [Mattermost system requirements](http://docs.mattermost.org/install/Requirements/index.html).
- [Production Install on Ubuntu 14.04](https://github.com/mattermost/platform/blob/release-1.0.0/doc/install/prod-ubuntu.md) - Install Mattermost for production environments.
@@ -90,7 +90,7 @@ Joining the Mattermost community is a great way to build relationships with othe
#### Contribute
- Share [feature ideas](http://www.mattermost.org/feature-requests/) with the Mattermost community
-- Review the [Mattermost Code Contribution Guidelines](doc/developer/code-contribution.md) to submit patches for the core product
+- Review the [Mattermost Code Contribution Guidelines](http://docs.mattermost.org/developer/Code-Contribution-Guidelines/index.html) to submit patches for the core product
- Consider building tools that help developers and IT professionals manage Mattermost more effectively (API documentation coming in Beta2)
#### Have other ideas or suggestions?
diff --git a/api/channel.go b/api/channel.go
index 6494e3528..5e13fa18a 100644
--- a/api/channel.go
+++ b/api/channel.go
@@ -281,7 +281,7 @@ func getChannels(c *Context, w http.ResponseWriter, r *http.Request) {
// lets make sure the user is valid
if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil {
c.Err = result.Err
- c.RemoveSessionCookie(w)
+ c.RemoveSessionCookie(w, r)
l4g.Error("Error in getting users profile for id=%v forcing logout", c.Session.UserId)
return
}
diff --git a/api/context.go b/api/context.go
index 02c3dc902..e80582b2a 100644
--- a/api/context.go
+++ b/api/context.go
@@ -137,7 +137,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
if session == nil || session.IsExpired() {
- c.RemoveSessionCookie(w)
+ c.RemoveSessionCookie(w, r)
c.Err = model.NewAppError("ServeHTTP", "Invalid or expired session, please login again.", "token="+token)
c.Err.StatusCode = http.StatusUnauthorized
} else if !session.IsOAuth && isTokenFromQueryString {
@@ -303,7 +303,6 @@ func (c *Context) HasSystemAdminPermissions(where string) bool {
}
func (c *Context) IsSystemAdmin() bool {
- // TODO XXX FIXME && IsPrivateIpAddress(c.IpAddress)
if model.IsInRole(c.Session.Roles, model.ROLE_SYSTEM_ADMIN) {
return true
}
@@ -317,7 +316,7 @@ func (c *Context) IsTeamAdmin() bool {
return false
}
-func (c *Context) RemoveSessionCookie(w http.ResponseWriter) {
+func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) {
sessionCache.Remove(c.Session.Token)
@@ -330,6 +329,21 @@ func (c *Context) RemoveSessionCookie(w http.ResponseWriter) {
}
http.SetCookie(w, cookie)
+
+ multiToken := ""
+ if oldMultiCookie, err := r.Cookie(model.MULTI_SESSION_TOKEN); err == nil {
+ multiToken = oldMultiCookie.Value
+ }
+
+ multiCookie := &http.Cookie{
+ Name: model.MULTI_SESSION_TOKEN,
+ Value: strings.TrimSpace(strings.Replace(multiToken, c.Session.Token, "", -1)),
+ Path: "/",
+ MaxAge: model.SESSION_TIME_WEB_IN_SECS,
+ HttpOnly: true,
+ }
+
+ http.SetCookie(w, multiCookie)
}
func (c *Context) SetInvalidParam(where string, name string) {
@@ -346,7 +360,7 @@ func (c *Context) setTeamURL(url string, valid bool) {
c.teamURLValid = valid
}
-func (c *Context) setTeamURLFromSession() {
+func (c *Context) SetTeamURLFromSession() {
if result := <-Srv.Store.Team().Get(c.Session.TeamId); result.Err == nil {
c.setTeamURL(c.GetSiteURL()+"/"+result.Data.(*model.Team).Name, true)
}
@@ -362,7 +376,7 @@ func (c *Context) GetTeamURLFromTeam(team *model.Team) string {
func (c *Context) GetTeamURL() string {
if !c.teamURLValid {
- c.setTeamURLFromSession()
+ c.SetTeamURLFromSession()
if !c.teamURLValid {
l4g.Debug("TeamURL accessed when not valid. Team URL should not be used in api functions or those that are team independent")
}
diff --git a/api/user.go b/api/user.go
index ed3576a30..2d7dd9ab1 100644
--- a/api/user.go
+++ b/api/user.go
@@ -394,6 +394,41 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User,
http.SetCookie(w, sessionCookie)
+ multiToken := ""
+ if originalMultiSessionCookie, err := r.Cookie(model.MULTI_SESSION_TOKEN); err == nil {
+ multiToken = originalMultiSessionCookie.Value
+ }
+
+ // Attempt to clean all the old tokens or duplicate tokens
+ if len(multiToken) > 0 {
+ tokens := strings.Split(multiToken, " ")
+
+ multiToken = ""
+ seen := make(map[string]string)
+ seen[session.TeamId] = session.TeamId
+ for _, token := range tokens {
+ if sr := <-Srv.Store.Session().Get(token); sr.Err == nil {
+ s := sr.Data.(*model.Session)
+ if !s.IsExpired() && seen[s.TeamId] == "" {
+ multiToken += " " + token
+ seen[s.TeamId] = s.TeamId
+ }
+ }
+ }
+ }
+
+ multiToken = strings.TrimSpace(session.Token + " " + multiToken)
+
+ multiSessionCookie := &http.Cookie{
+ Name: model.MULTI_SESSION_TOKEN,
+ Value: multiToken,
+ Path: "/",
+ MaxAge: maxAge,
+ HttpOnly: true,
+ }
+
+ http.SetCookie(w, multiSessionCookie)
+
c.Session = *session
c.LogAuditWithUserId(user.Id, "success")
}
@@ -514,7 +549,7 @@ func logout(c *Context, w http.ResponseWriter, r *http.Request) {
func Logout(c *Context, w http.ResponseWriter, r *http.Request) {
c.LogAudit("")
- c.RemoveSessionCookie(w)
+ c.RemoveSessionCookie(w, r)
if result := <-Srv.Store.Session().Remove(c.Session.Id); result.Err != nil {
c.Err = result.Err
return
@@ -529,7 +564,7 @@ func getMe(c *Context, w http.ResponseWriter, r *http.Request) {
if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil {
c.Err = result.Err
- c.RemoveSessionCookie(w)
+ c.RemoveSessionCookie(w, r)
l4g.Error("Error in getting users profile for id=%v forcing logout", c.Session.UserId)
return
} else if HandleEtag(result.Data.(*model.User).Etag(), w, r) {
diff --git a/config/config.json b/config/config.json
index 38acee85a..b14175372 100644
--- a/config/config.json
+++ b/config/config.json
@@ -6,6 +6,8 @@
"GoogleDeveloperKey": "",
"EnableOAuthServiceProvider": false,
"EnableIncomingWebhooks": true,
+ "EnablePostUsernameOverride": false,
+ "EnablePostIconOverride": false,
"EnableTesting": false
},
"TeamSettings": {
@@ -75,7 +77,8 @@
},
"PrivacySettings": {
"ShowEmailAddress": true,
- "ShowFullName": true
+ "ShowFullName": true,
+ "EnableDiagnostic": false
},
"GitLabSettings": {
"Enable": false,
@@ -86,4 +89,4 @@
"TokenEndpoint": "",
"UserApiEndpoint": ""
}
-} \ No newline at end of file
+}
diff --git a/doc/README.md b/doc/README.md
index dc1591705..d711b5d53 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -1,31 +1,26 @@
# Mattermost Documentation
-## General Information
+## Installation
-- [Mattermost Release Numbering Scheme](install/release-numbering.md)
+- [AWS Elastic Beanstalk Setup](install/Amazon-Elastic-Beanstalk.md)
+- [Docker Single Container Preview Setup](install/Docker-Single-Container.md)
+- [Production Ubuntu Setup](install/Production-Ubuntu.md)
+- [Mattermost Release Numbering Scheme](install/Release-Numbering.md)
+- [Software and Hardware Requirements](install/Requirements.md)
+- [SMTP Email Setup](install/SMTP-Email-Setup.md)
-## Administrator Documentation
+## Integrations
-### Installation
-
-- [Software and Hardware Requirements](install/requirements.md)
-- [Production Installation](install/prod-ubuntu.md)
-- [Local Machine Setup ](install/single-container-install.md)
-- [AWS Elastic Beanstalk Setup](install/aws-ebs-setup.md)
-- [Developer Machine Setup](install/dev-setup.md)
-
-### Configuration
-
-- [GitLab SSO Configuration](integrations/sso/gitlab-sso.md)
-- [SMTP Email Setup](config/smtp-email-setup.md)
+- [GitLab SSO Configuration](integrations/Single-Sign-On/Gitlab.md)
+- [Incoming Webhooks](integrations/webhooks/Incoming.md)
## Developer Documentation
-- [Code Contribution Guidelines](developer/code-contribution.md)
-- [Developer Machine Setup](install/dev-setup.md)
-- [Mattermost Style Guide](developer/style-guide.md)
+- [Code Contribution Guidelines](developer/Code-Contribution-Guidelines.md)
+- [Developer Machine Setup](developer/Setup.md)
+- [Mattermost Style Guide](developer/Style-Guide.md)
-## End User Help
+## Usage Help
-- [Mattermost Markdown Formatting](help/enduser/markdown.md)
-- [Slack Import](https://github.com/mattermost/platform/blob/master/doc/import/slack-import.md)
+- [Slack Import](usage/Slack-Import.md)
+- [Mattermost Markdown Formatting](usage/Markdown.md)
diff --git a/doc/developer/code-contribution.md b/doc/developer/Code-Contribution-Guidelines.md
index 51521be1b..80676f107 100644
--- a/doc/developer/code-contribution.md
+++ b/doc/developer/Code-Contribution-Guidelines.md
@@ -23,7 +23,7 @@ git checkout -b <branch name>
## Programming and Testing
-1. Please review the [Mattermost Style Guide](style-guide.md) prior to making changes.
+1. Please review the [Mattermost Style Guide](Style-Guide.md) prior to making changes.
To keep code clean and well structured, Mattermost uses ESLint to check that pull requests adhere to style guidelines for React. Code will need to follow Mattermost's React style guidelines in order to pass the automated build tests when a pull request is submitted.
diff --git a/doc/install/dev-setup.md b/doc/developer/Setup.md
index c63bde512..c63bde512 100644
--- a/doc/install/dev-setup.md
+++ b/doc/developer/Setup.md
diff --git a/doc/developer/style-guide.md b/doc/developer/Style-Guide.md
index a06f9e29b..a06f9e29b 100644
--- a/doc/developer/style-guide.md
+++ b/doc/developer/Style-Guide.md
diff --git a/doc/favicon.ico b/doc/favicon.ico
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/doc/favicon.ico
diff --git a/doc/install/aws-ebs-setup.md b/doc/install/Amazon-Elastic-Beanstalk.md
index e186fa9c1..0416b67ea 100644
--- a/doc/install/aws-ebs-setup.md
+++ b/doc/install/Amazon-Elastic-Beanstalk.md
@@ -1,14 +1,14 @@
## AWS Elastic Beanstalk Setup (Docker)
-1. Create a new Elastic Beanstalk Docker application using the [Dockerrun.aws.zip](/docker/0.7/Dockerrun.aws.zip) file provided.
+1. Create a new Elastic Beanstalk Docker application using the [Dockerrun.aws.zip](https://github.com/mattermost/platform/raw/master/docker/1.0/Dockerrun.aws.zip) file provided.
1. From the AWS console select Elastic Beanstalk.
2. Select "Create New Application" from the top right.
3. Name the application and press next.
4. Select "Create a web server" environment.
5. If asked, select create an IAM role and instance profile and press next.
6. For predefined configuration select under Generic: Docker. For environment type select single instance.
- 7. For application source, select upload your own and upload Dockerrun.aws.zip from [Dockerrun.aws.zip](/docker/0.7/Dockerrun.aws.zip). Everything else may be left at default.
+ 7. For application source, select upload your own and upload Dockerrun.aws.zip from [Dockerrun.aws.zip](https://github.com/mattermost/platform/raw/master/docker/1.0/Dockerrun.aws.zip). Everything else may be left at default.
8. Select an environment name, this is how you will refer to your environment. Make sure the URL is available then press next.
9. The options on the additional resources page may be left at default unless you wish to change them. Press Next.
10. On the configuration details place. Select an instance type of t2.small or larger.
@@ -24,4 +24,4 @@
### (Recommended) Enable Email
The default single-container Docker instance for Mattermost is designed for product evaluation, and sets `ByPassEmail=true` so the product can run without enabling email, when doing so maybe difficult.
- To see the product's full functionality, [enabling SMTP email is recommended](doc/config/smtp-email-setup.md).
+ To see the product's full functionality, [enabling SMTP email is recommended](SMTP-Email-Setup.md).
diff --git a/doc/install/single-container-install.md b/doc/install/Docker-Single-Container.md
index 304467678..4b952cd71 100644
--- a/doc/install/single-container-install.md
+++ b/doc/install/Docker-Single-Container.md
@@ -75,7 +75,7 @@ The following install instructions are for single-container installs of Mattermo
## Configuration Settings
-There are a few configuration settings you might want to adjust when setting up your instance of Mattermost. You can edit them in [config/config.json](config/config.json) or [docker/0.6/config_docker.json](docker/0.6/config_docker.json) if you're running a Docker instance.
+There are a few configuration settings you might want to adjust when setting up your instance of Mattermost. You can edit them in `config.json` or `config_docker.json` if you're running a Docker instance.
* *EmailSettings*:*ByPassEmail* - If this is set to true, then users on the system will not need to verify their email addresses when signing up. In addition, no emails will ever be sent.
* *ServiceSettings*:*UseLocalStorage* - If this is set to true, then your Mattermost server will store uploaded files in the storage directory specified by *StorageDirectory*. *StorageDirectory* must be set if *UseLocalStorage* is set to true.
@@ -86,7 +86,7 @@ There are a few configuration settings you might want to adjust when setting up
The default single-container Docker instance for Mattermost is designed for product evaluation, and sets `ByPassEmail=true` so the product can run without enabling email, when doing so maybe difficult.
-To see the product's full functionality, [enabling SMTP email is recommended](/doc/config/smtp-email-setup.md).
+To see the product's full functionality, [enabling SMTP email is recommended](SMTP-Email-Setup.md).
## Upgrading Mattermost
diff --git a/doc/install/prod-ubuntu.md b/doc/install/Production-Ubuntu.md
index 0f1373c1a..0f1373c1a 100644
--- a/doc/install/prod-ubuntu.md
+++ b/doc/install/Production-Ubuntu.md
diff --git a/doc/install/release-numbering.md b/doc/install/Release-Numbering.md
index 71374f7ef..71374f7ef 100644
--- a/doc/install/release-numbering.md
+++ b/doc/install/Release-Numbering.md
diff --git a/doc/install/requirements.md b/doc/install/Requirements.md
index 1e0a12fb9..1e0a12fb9 100644
--- a/doc/install/requirements.md
+++ b/doc/install/Requirements.md
diff --git a/doc/config/smtp-email-setup.md b/doc/install/SMTP-Email-Setup.md
index 86e2bb20e..86e2bb20e 100644
--- a/doc/config/smtp-email-setup.md
+++ b/doc/install/SMTP-Email-Setup.md
diff --git a/doc/integrations/sso/gitlab-sso.md b/doc/integrations/Single-Sign-On/Gitlab.md
index 6110db504..87889df66 100644
--- a/doc/integrations/sso/gitlab-sso.md
+++ b/doc/integrations/Single-Sign-On/Gitlab.md
@@ -6,6 +6,8 @@ The following steps can be used to configure Mattermost to use GitLab as a singl
2. Add a new application called "Mattermost" with the following as Redirect URIs:
* `<your-mattermost-url>/login/gitlab/complete` (example: http://localhost:8065/login/gitlab/complete)
* `<your-mattermost-url>/signup/gitlab/complete`
+
+ (Note: If your GitLab instance is set up to use SSL, your URIs must begin with https://. Otherwise, use http://).
3. Submit the application and copy the given _Id_ and _Secret_ into the appropriate _SSOSettings_ fields in config/config.json
diff --git a/doc/integrations/webhook/incoming.md b/doc/integrations/webhooks/Incoming.md
index e391cba92..6e25f182e 100644
--- a/doc/integrations/webhook/incoming.md
+++ b/doc/integrations/webhooks/Incoming.md
@@ -25,7 +25,7 @@ In addition, if `Content-Type` is specified as `application/json` in the headers
{"text": "Hello, this is some text."}
```
-It is also possible to post richly formatted messages using [Markdown](../../help/enduser/markdown.md).
+It is also possible to post richly formatted messages using [Markdown](../../usage/Markdown.md).
```
payload={"text": "# A Header\nThe _text_ below **the** header."}
```
diff --git a/doc/help/enduser/markdown.md b/doc/usage/Markdown.md
index 9e2342a0b..9e2342a0b 100644
--- a/doc/help/enduser/markdown.md
+++ b/doc/usage/Markdown.md
diff --git a/doc/import/slack-import.md b/doc/usage/Slack-Import.md
index c30de0567..c30de0567 100644
--- a/doc/import/slack-import.md
+++ b/doc/usage/Slack-Import.md
diff --git a/docker/1.0/Dockerfile b/docker/1.0/Dockerfile
index 8eeef8fb7..be87f9f89 100644
--- a/docker/1.0/Dockerfile
+++ b/docker/1.0/Dockerfile
@@ -34,7 +34,7 @@ VOLUME /var/lib/mysql
WORKDIR /mattermost
# Copy over files
-ADD https://github.com/mattermost/platform/releases/download/v1.0.0-rc2/mattermost.tar.gz /
+ADD https://github.com/mattermost/platform/releases/download/v1.0.0/mattermost.tar.gz /
RUN tar -zxvf /mattermost.tar.gz --strip-components=1 && rm /mattermost.tar.gz
ADD config_docker.json /
ADD docker-entry.sh /
diff --git a/docker/dev/config_docker.json b/docker/dev/config_docker.json
index 733267f74..ef91a21ea 100644
--- a/docker/dev/config_docker.json
+++ b/docker/dev/config_docker.json
@@ -6,6 +6,8 @@
"GoogleDeveloperKey": "",
"EnableOAuthServiceProvider": false,
"EnableIncomingWebhooks": true,
+ "EnablePostUsernameOverride": false,
+ "EnablePostIconOverride": false,
"EnableTesting": false
},
"TeamSettings": {
@@ -75,7 +77,8 @@
},
"PrivacySettings": {
"ShowEmailAddress": true,
- "ShowFullName": true
+ "ShowFullName": true,
+ "EnableDiagnostic": false
},
"GitLabSettings": {
"Enable": false,
diff --git a/docker/local/config_docker.json b/docker/local/config_docker.json
index 733267f74..ef91a21ea 100644
--- a/docker/local/config_docker.json
+++ b/docker/local/config_docker.json
@@ -6,6 +6,8 @@
"GoogleDeveloperKey": "",
"EnableOAuthServiceProvider": false,
"EnableIncomingWebhooks": true,
+ "EnablePostUsernameOverride": false,
+ "EnablePostIconOverride": false,
"EnableTesting": false
},
"TeamSettings": {
@@ -75,7 +77,8 @@
},
"PrivacySettings": {
"ShowEmailAddress": true,
- "ShowFullName": true
+ "ShowFullName": true,
+ "EnableDiagnostic": false
},
"GitLabSettings": {
"Enable": false,
diff --git a/mattermost.go b/mattermost.go
index 94bbe344d..e78e8d04a 100644
--- a/mattermost.go
+++ b/mattermost.go
@@ -8,6 +8,8 @@ import (
"fmt"
"os"
"os/signal"
+ "runtime"
+ "strconv"
"strings"
"syscall"
"time"
@@ -61,6 +63,8 @@ func main() {
manualtesting.InitManualTesting()
}
+ diagnosticsJob()
+
// wait for kill signal before attempting to gracefully shutdown
// the running service
c := make(chan os.Signal)
@@ -71,6 +75,53 @@ func main() {
}
}
+func diagnosticsJob() {
+ go func() {
+ for {
+ if utils.Cfg.PrivacySettings.EnableDiagnostic && !model.IsOfficalBuild() {
+ if result := <-api.Srv.Store.System().Get(); result.Err == nil {
+ props := result.Data.(model.StringMap)
+ lastTime, _ := strconv.ParseInt(props["LastDiagnosticTime"], 10, 0)
+ currentTime := model.GetMillis()
+
+ if (currentTime - lastTime) > 1000*60*60*24*7 {
+ l4g.Info("Sending error and diagnostic information to mattermost")
+
+ id := props["DiagnosticId"]
+ if len(id) == 0 {
+ id = model.NewId()
+ systemId := &model.System{Name: "DiagnosticId", Value: id}
+ <-api.Srv.Store.System().Save(systemId)
+ }
+
+ systemLastTime := &model.System{Name: "LastDiagnosticTime", Value: strconv.FormatInt(currentTime, 10)}
+ if lastTime == 0 {
+ <-api.Srv.Store.System().Save(systemLastTime)
+ } else {
+ <-api.Srv.Store.System().Update(systemLastTime)
+ }
+
+ m := make(map[string]string)
+ m[utils.PROP_DIAGNOSTIC_ID] = id
+ m[utils.PROP_DIAGNOSTIC_BUILD] = model.CurrentVersion + "." + model.BuildNumber
+ m[utils.PROP_DIAGNOSTIC_DATABASE] = utils.Cfg.SqlSettings.DriverName
+ m[utils.PROP_DIAGNOSTIC_OS] = runtime.GOOS
+ m[utils.PROP_DIAGNOSTIC_CATEGORY] = utils.VAL_DIAGNOSTIC_CATEGORY_DEFALUT
+
+ if ucr := <-api.Srv.Store.User().GetTotalUsersCount(); ucr.Err == nil {
+ m[utils.PROP_DIAGNOSTIC_USER_COUNT] = strconv.FormatInt(ucr.Data.(int64), 10)
+ }
+
+ utils.SendDiagnostic(m)
+ }
+ }
+ }
+
+ time.Sleep(time.Hour * 24)
+ }
+ }()
+}
+
func parseCmds() {
flag.Usage = func() {
fmt.Fprintln(os.Stderr, usage)
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 000000000..d7113e03e
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,11 @@
+site_name: Mattermost Documentation
+site_url: http://docs.mattermost.org
+repo_url: https://github.com/mattermost/platform
+repo_name: GitHub
+site_favicon: favicon.ico
+copyright: "Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved."
+strict: true
+docs_dir: doc
+site_dir: documentation-html
+use_directory_urls: false
+theme: mkdocs
diff --git a/model/config.go b/model/config.go
index 5d822e263..c67b36063 100644
--- a/model/config.go
+++ b/model/config.go
@@ -29,6 +29,8 @@ type ServiceSettings struct {
GoogleDeveloperKey string
EnableOAuthServiceProvider bool
EnableIncomingWebhooks bool
+ EnablePostUsernameOverride bool
+ EnablePostIconOverride bool
EnableTesting bool
}
@@ -110,6 +112,7 @@ type RateLimitSettings struct {
type PrivacySettings struct {
ShowEmailAddress bool
ShowFullName bool
+ EnableDiagnostic bool
}
type TeamSettings struct {
diff --git a/model/session.go b/model/session.go
index 3c7c75eb4..bb4d987a7 100644
--- a/model/session.go
+++ b/model/session.go
@@ -10,6 +10,7 @@ import (
const (
SESSION_TOKEN = "MMSID"
+ MULTI_SESSION_TOKEN = "MMSIDMU"
SESSION_TIME_WEB_IN_DAYS = 30
SESSION_TIME_WEB_IN_SECS = 60 * 60 * 24 * SESSION_TIME_WEB_IN_DAYS
SESSION_TIME_MOBILE_IN_DAYS = 30
diff --git a/model/utils.go b/model/utils.go
index e19cceba5..93b8c4512 100644
--- a/model/utils.go
+++ b/model/utils.go
@@ -163,6 +163,7 @@ var reservedName = []string{
"post",
"cluster",
"api",
+ "oauth",
}
var wwwStart = regexp.MustCompile(`^www`)
diff --git a/model/version.go b/model/version.go
index 233fc3747..efa1697db 100644
--- a/model/version.go
+++ b/model/version.go
@@ -67,6 +67,10 @@ func GetPreviousVersion(currentVersion string) (int64, int64) {
return 0, 0
}
+func IsOfficalBuild() bool {
+ return BuildNumber != "_BUILD_NUMBER_"
+}
+
func IsCurrentVersion(versionToCheck string) bool {
currentMajor, currentMinor, _ := SplitVersion(CurrentVersion)
toCheckMajor, toCheckMinor, _ := SplitVersion(versionToCheck)
diff --git a/utils/config.go b/utils/config.go
index 3218211e3..44c4c43af 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -184,6 +184,8 @@ func getClientProperties(c *model.Config) map[string]string {
props["SegmentDeveloperKey"] = c.ServiceSettings.SegmentDeveloperKey
props["GoogleDeveloperKey"] = c.ServiceSettings.GoogleDeveloperKey
props["EnableIncomingWebhooks"] = strconv.FormatBool(c.ServiceSettings.EnableIncomingWebhooks)
+ props["EnablePostUsernameOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostUsernameOverride)
+ props["EnablePostIconOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostIconOverride)
props["SendEmailNotifications"] = strconv.FormatBool(c.EmailSettings.SendEmailNotifications)
props["EnableSignUpWithEmail"] = strconv.FormatBool(c.EmailSettings.EnableSignUpWithEmail)
diff --git a/utils/diagnostic.go b/utils/diagnostic.go
new file mode 100644
index 000000000..9a61ae934
--- /dev/null
+++ b/utils/diagnostic.go
@@ -0,0 +1,45 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package utils
+
+import (
+ "net/http"
+
+ l4g "code.google.com/p/log4go"
+
+ "github.com/mattermost/platform/model"
+)
+
+const (
+ PROP_DIAGNOSTIC_ID = "id"
+ PROP_DIAGNOSTIC_CATEGORY = "c"
+ VAL_DIAGNOSTIC_CATEGORY_DEFALUT = "d"
+ PROP_DIAGNOSTIC_BUILD = "b"
+ PROP_DIAGNOSTIC_DATABASE = "db"
+ PROP_DIAGNOSTIC_OS = "os"
+ PROP_DIAGNOSTIC_USER_COUNT = "uc"
+)
+
+func SendDiagnostic(data model.StringMap) *model.AppError {
+ if Cfg.PrivacySettings.EnableDiagnostic && !model.IsOfficalBuild() {
+
+ query := "?"
+ for name, value := range data {
+ if len(query) > 1 {
+ query += "&"
+ }
+
+ query += name + "=" + UrlEncode(value)
+ }
+
+ res, err := http.Get("http://d7zmvsa9e04kk.cloudfront.net/i" + query)
+ if err != nil {
+ l4g.Error("Failed to send diagnostics %v", err.Error())
+ }
+
+ res.Body.Close()
+ }
+
+ return nil
+}
diff --git a/web/react/.eslintrc b/web/react/.eslintrc
index c0d0bb200..c4167829d 100644
--- a/web/react/.eslintrc
+++ b/web/react/.eslintrc
@@ -42,12 +42,13 @@
"valid-typeof": 2,
"block-scoped-var": 2,
- "complexity": [1, 8],
+ "complexity": [0, 8],
"consistent-return": 2,
"curly": [2, "all"],
"dot-location": [2, "object"],
"dot-notation": 2,
"eqeqeq": [2, "smart"],
+ "global-require": 2,
"guard-for-in": 2,
"no-alert": 2,
"no-array-constructor": 2,
@@ -109,30 +110,34 @@
"func-names": 2,
"func-style": [2, "declaration"],
"indent": [2, 4, {"SwitchCase": 0}],
+ "jsx-quotes": [2, "prefer-single"],
"key-spacing": [2, {"beforeColon": false, "afterColon": true}],
- "lines-around-comment": [2, { "beforeBlockComment": true, "beforeLineComment": true, "allowBlockStart": true, "allowBlockEnd": true }],
"linebreak-style": 2,
+ "lines-around-comment": [2, { "beforeBlockComment": true, "beforeLineComment": true, "allowBlockStart": true, "allowBlockEnd": true }],
"new-cap": 2,
"new-parens": 2,
"no-lonely-if": 2,
"no-mixed-spaces-and-tabs": 2,
"no-multiple-empty-lines": [2, {"max": 1}],
+ "no-negated-condition": 2,
+ "no-nested-ternary": 2,
"no-spaced-func": 2,
- "no-ternary": 2,
+ "no-ternary": 0,
"no-trailing-spaces": [2, { "skipBlankLines": false }],
"no-underscore-dangle": 2,
- "no-unneeded-ternary": 2,
+ "no-unneeded-ternary": [2, {"defaultAssignment": false}],
"object-curly-spacing": [2, "never"],
"one-var": [2, "never"],
"operator-linebreak": [2, "after"],
"padded-blocks": [2, "never"],
"quote-props": [2, "as-needed"],
"quotes": [2, "single", "avoid-escape"],
- "semi-spacing": [2, {"before": false, "after": true}],
"semi": [2, "always"],
+ "semi-spacing": [2, {"before": false, "after": true}],
"space-after-keywords": [2, "always"],
"space-before-blocks": [2, "always"],
"space-before-function-paren": [2, "never"],
+ "space-before-keywords": [2, "always"],
"space-in-parens": [2, "never"],
"space-infix-ops": 2,
"space-return-throw-case": 2,
@@ -167,7 +172,6 @@
"react/jsx-no-duplicate-props": [2, { "ignoreCase": false }],
"react/jsx-no-literals": 1,
"react/jsx-no-undef": 2,
- "react/jsx-quotes": [2, "single", "avoid-escape"],
"react/jsx-uses-react": 2,
"react/jsx-uses-vars": 2,
"react/no-danger": 0,
@@ -179,6 +183,7 @@
"react/prop-types": 2,
"react/self-closing-comp": 2,
"react/sort-comp": 0,
- "react/wrap-multilines": 2
+ "react/wrap-multilines": 2,
+ "react/no-direct-mutation-state": 2
}
}
diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx
index a080150dd..0dfd36717 100644
--- a/web/react/components/access_history_modal.jsx
+++ b/web/react/components/access_history_modal.jsx
@@ -12,25 +12,31 @@ export default class AccessHistoryModal extends React.Component {
this.onAuditChange = this.onAuditChange.bind(this);
this.handleMoreInfo = this.handleMoreInfo.bind(this);
+ this.onHide = this.onHide.bind(this);
+ this.onShow = this.onShow.bind(this);
- this.state = this.getStateFromStoresForAudits();
- this.state.moreInfo = [];
+ let state = this.getStateFromStoresForAudits();
+ state.moreInfo = [];
+
+ this.state = state;
}
getStateFromStoresForAudits() {
return {
audits: UserStore.getAudits()
};
}
+ onShow() {
+ AsyncClient.getAudits();
+ }
+ onHide() {
+ $('#user_settings').modal('show');
+ this.setState({moreInfo: []});
+ }
componentDidMount() {
UserStore.addAuditsChangeListener(this.onAuditChange);
- $(React.findDOMNode(this.refs.modal)).on('shown.bs.modal', function show() {
- AsyncClient.getAudits();
- });
+ $(React.findDOMNode(this.refs.modal)).on('shown.bs.modal', this.onShow);
- $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', function hide() {
- $('#user_settings').modal('show');
- this.setState({moreInfo: []});
- }.bind(this));
+ $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.onHide);
}
componentWillUnmount() {
UserStore.removeAuditsChangeListener(this.onAuditChange);
diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx
index fe40385a0..aee2541b5 100644
--- a/web/react/components/activity_log_modal.jsx
+++ b/web/react/components/activity_log_modal.jsx
@@ -14,9 +14,13 @@ export default class ActivityLogModal extends React.Component {
this.submitRevoke = this.submitRevoke.bind(this);
this.onListenerChange = this.onListenerChange.bind(this);
this.handleMoreInfo = this.handleMoreInfo.bind(this);
+ this.onHide = this.onHide.bind(this);
+ this.onShow = this.onShow.bind(this);
- this.state = this.getStateFromStores();
- this.state.moreInfo = [];
+ let state = this.getStateFromStores();
+ state.moreInfo = [];
+
+ this.state = state;
}
getStateFromStores() {
return {
@@ -38,16 +42,18 @@ export default class ActivityLogModal extends React.Component {
}.bind(this)
);
}
+ onShow() {
+ AsyncClient.getSessions();
+ }
+ onHide() {
+ $('#user_settings').modal('show');
+ this.setState({moreInfo: []});
+ }
componentDidMount() {
UserStore.addSessionsChangeListener(this.onListenerChange);
- $(React.findDOMNode(this.refs.modal)).on('shown.bs.modal', function handleShow() {
- AsyncClient.getSessions();
- });
+ $(React.findDOMNode(this.refs.modal)).on('shown.bs.modal', this.onShow);
- $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', function handleHide() {
- $('#user_settings').modal('show');
- this.setState({moreInfo: []});
- }.bind(this));
+ $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.onHide);
}
componentWillUnmount() {
UserStore.removeSessionsChangeListener(this.onListenerChange);
diff --git a/web/react/components/admin_console/privacy_settings.jsx b/web/react/components/admin_console/privacy_settings.jsx
index affd8ae11..c74d321e6 100644
--- a/web/react/components/admin_console/privacy_settings.jsx
+++ b/web/react/components/admin_console/privacy_settings.jsx
@@ -30,6 +30,7 @@ export default class PrivacySettings extends React.Component {
var config = this.props.config;
config.PrivacySettings.ShowEmailAddress = React.findDOMNode(this.refs.ShowEmailAddress).checked;
config.PrivacySettings.ShowFullName = React.findDOMNode(this.refs.ShowFullName).checked;
+ config.PrivacySettings.EnableDiagnostic = React.findDOMNode(this.refs.EnableDiagnostic).checked;
Client.saveConfig(
config,
@@ -137,6 +138,39 @@ export default class PrivacySettings extends React.Component {
</div>
<div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableDiagnostic'
+ >
+ {'Send Error and Diagnostic: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableDiagnostic'
+ value='true'
+ ref='EnableDiagnostic'
+ defaultChecked={this.props.config.PrivacySettings.EnableDiagnostic}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableDiagnostic'
+ value='false'
+ defaultChecked={!this.props.config.PrivacySettings.EnableDiagnostic}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, The server will periodically send error and diagnostic information to Mattermost.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
<div className='col-sm-12'>
{serverError}
<button
diff --git a/web/react/components/admin_console/service_settings.jsx b/web/react/components/admin_console/service_settings.jsx
index 245ffa871..b2d1b7b4d 100644
--- a/web/react/components/admin_console/service_settings.jsx
+++ b/web/react/components/admin_console/service_settings.jsx
@@ -37,6 +37,8 @@ export default class ServiceSettings extends React.Component {
config.ServiceSettings.GoogleDeveloperKey = React.findDOMNode(this.refs.GoogleDeveloperKey).value.trim();
//config.ServiceSettings.EnableOAuthServiceProvider = React.findDOMNode(this.refs.EnableOAuthServiceProvider).checked;
config.ServiceSettings.EnableIncomingWebhooks = React.findDOMNode(this.refs.EnableIncomingWebhooks).checked;
+ config.ServiceSettings.EnablePostUsernameOverride = React.findDOMNode(this.refs.EnablePostUsernameOverride).checked;
+ config.ServiceSettings.EnablePostIconOverride = React.findDOMNode(this.refs.EnablePostIconOverride).checked;
config.ServiceSettings.EnableTesting = React.findDOMNode(this.refs.EnableTesting).checked;
var MaximumLoginAttempts = 10;
@@ -199,7 +201,73 @@ export default class ServiceSettings extends React.Component {
/>
{'false'}
</label>
- <p className='help-text'>{'When true, incoming webhooks will be allowed.'}</p>
+ <p className='help-text'>{'When true, incoming webhooks will be allowed. To help combat phishing attacks, all posts from webhooks will be labelled by a BOT tag.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnablePostUsernameOverride'
+ >
+ {'Enable Overriding Usernames from Webhooks: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnablePostUsernameOverride'
+ value='true'
+ ref='EnablePostUsernameOverride'
+ defaultChecked={this.props.config.ServiceSettings.EnablePostUsernameOverride}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnablePostUsernameOverride'
+ value='false'
+ defaultChecked={!this.props.config.ServiceSettings.EnablePostUsernameOverride}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, webhooks will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnablePostIconOverride'
+ >
+ {'Enable Overriding Icon from Webhooks: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnablePostIconOverride'
+ value='true'
+ ref='EnablePostIconOverride'
+ defaultChecked={this.props.config.ServiceSettings.EnablePostIconOverride}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnablePostIconOverride'
+ value='false'
+ defaultChecked={!this.props.config.ServiceSettings.EnablePostIconOverride}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, webhooks will be allowed to change the icon they post with. Note, combined with allowing username overriding, this could open users up to phishing attacks.'}</p>
</div>
</div>
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx
index b81936b57..f15974d35 100644
--- a/web/react/components/channel_header.jsx
+++ b/web/react/components/channel_header.jsx
@@ -132,7 +132,26 @@ export default class ChannelHeader extends React.Component {
}
let dropdownContents = [];
- if (!isDirect) {
+ if (isDirect) {
+ dropdownContents.push(
+ <li
+ key='edit_description_direct'
+ role='presentation'
+ >
+ <a
+ role='menuitem'
+ href='#'
+ data-toggle='modal'
+ data-target='#edit_channel'
+ data-desc={channel.description}
+ data-title={channel.display_name}
+ data-channelid={channel.id}
+ >
+ Set Channel Description...
+ </a>
+ </li>
+ );
+ } else {
dropdownContents.push(
<li
key='view_info'
@@ -276,25 +295,6 @@ export default class ChannelHeader extends React.Component {
</li>
);
}
- } else {
- dropdownContents.push(
- <li
- key='edit_description_direct'
- role='presentation'
- >
- <a
- role='menuitem'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-desc={channel.description}
- data-title={channel.display_name}
- data-channelid={channel.id}
- >
- Set Channel Description...
- </a>
- </li>
- );
}
return (
diff --git a/web/react/components/channel_members.jsx b/web/react/components/channel_members.jsx
index 1eda6a104..53c854eb7 100644
--- a/web/react/components/channel_members.jsx
+++ b/web/react/components/channel_members.jsx
@@ -15,6 +15,8 @@ export default class ChannelMembers extends React.Component {
this.getStateFromStores = this.getStateFromStores.bind(this);
this.onChange = this.onChange.bind(this);
this.handleRemove = this.handleRemove.bind(this);
+ this.onHide = this.onHide.bind(this);
+ this.onShow = this.onShow.bind(this);
this.state = this.getStateFromStores();
}
@@ -63,16 +65,18 @@ export default class ChannelMembers extends React.Component {
channelName: channelName
};
}
+ onHide() {
+ this.setState({renderMembers: false});
+ }
+ onShow() {
+ this.setState({renderMembers: true});
+ }
componentDidMount() {
ChannelStore.addExtraInfoChangeListener(this.onChange);
ChannelStore.addChangeListener(this.onChange);
- $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', function handleHide() {
- this.setState({renderMembers: false});
- }.bind(this));
+ $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.onHide);
- $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function handleShow() {
- this.setState({renderMembers: true});
- }.bind(this));
+ $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
}
componentWillUnmount() {
ChannelStore.removeExtraInfoChangeListener(this.onChange);
diff --git a/web/react/components/channel_notifications.jsx b/web/react/components/channel_notifications.jsx
index 45981b295..fed8e789e 100644
--- a/web/react/components/channel_notifications.jsx
+++ b/web/react/components/channel_notifications.jsx
@@ -23,6 +23,7 @@ export default class ChannelNotifications extends React.Component {
this.handleSubmitMarkUnreadLevel = this.handleSubmitMarkUnreadLevel.bind(this);
this.handleUpdateMarkUnreadLevel = this.handleUpdateMarkUnreadLevel.bind(this);
this.createMarkUnreadLevelSection = this.createMarkUnreadLevelSection.bind(this);
+ this.onShow = this.onShow.bind(this);
this.state = {
notifyLevel: '',
@@ -32,30 +33,29 @@ export default class ChannelNotifications extends React.Component {
activeSection: ''
};
}
+ onShow(e) {
+ var button = e.relatedTarget;
+ var channelId = button.getAttribute('data-channelid');
+ const member = ChannelStore.getMember(channelId);
+ var notifyLevel = member.notify_props.desktop;
+ var markUnreadLevel = member.notify_props.mark_unread;
+
+ this.setState({
+ notifyLevel,
+ markUnreadLevel,
+ title: button.getAttribute('data-title'),
+ channelId
+ });
+ }
componentDidMount() {
ChannelStore.addChangeListener(this.onListenerChange);
- $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function showModal(e) {
- var button = e.relatedTarget;
- var channelId = button.getAttribute('data-channelid');
-
- const member = ChannelStore.getMember(channelId);
- var notifyLevel = member.notify_props.desktop;
- var markUnreadLevel = member.notify_props.mark_unread;
-
- this.setState({
- notifyLevel,
- markUnreadLevel,
- title: button.getAttribute('data-title'),
- channelId: channelId
- });
- }.bind(this));
+ $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onListenerChange);
}
-
onListenerChange() {
if (!this.state.channelId) {
return;
@@ -76,7 +76,6 @@ export default class ChannelNotifications extends React.Component {
updateSection(section) {
this.setState({activeSection: section});
}
-
handleSubmitNotifyLevel() {
var channelId = this.state.channelId;
var notifyLevel = this.state.notifyLevel;
@@ -103,12 +102,10 @@ export default class ChannelNotifications extends React.Component {
}
);
}
-
handleUpdateNotifyLevel(notifyLevel) {
this.setState({notifyLevel});
React.findDOMNode(this.refs.modal).focus();
}
-
createNotifyLevelSection(serverError) {
var handleUpdateSection;
diff --git a/web/react/components/command_list.jsx b/web/react/components/command_list.jsx
index fea7085b7..e027e87ae 100644
--- a/web/react/components/command_list.jsx
+++ b/web/react/components/command_list.jsx
@@ -51,7 +51,7 @@ export default class CommandList extends React.Component {
this.setState({suggestions: data.suggestions, cmd: cmd});
}.bind(this),
function fail() {
- }
+ }
);
}
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index 5097b3aa5..9c233ea26 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -170,7 +170,9 @@ export default class CreateComment extends React.Component {
this.setState({uploadsInProgress: draft.uploadsInProgress, previews: draft.previews});
}
handleUploadError(err, clientId) {
- if (clientId !== -1) {
+ if (clientId === -1) {
+ this.setState({serverError: err});
+ } else {
let draft = PostStore.getCommentDraft(this.props.rootId);
const index = draft.uploadsInProgress.indexOf(clientId);
@@ -181,8 +183,6 @@ export default class CreateComment extends React.Component {
PostStore.storeCommentDraft(this.props.rootId, draft);
this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: err});
- } else {
- this.setState({serverError: err});
}
}
handleTextDrop(text) {
@@ -196,15 +196,15 @@ export default class CreateComment extends React.Component {
// id can either be the path of an uploaded file or the client id of an in progress upload
let index = previews.indexOf(id);
- if (index !== -1) {
- previews.splice(index, 1);
- } else {
+ if (index === -1) {
index = uploadsInProgress.indexOf(id);
if (index !== -1) {
uploadsInProgress.splice(index, 1);
this.refs.fileUpload.cancelUpload(id);
}
+ } else {
+ previews.splice(index, 1);
}
let draft = PostStore.getCommentDraft(this.props.rootId);
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index 0cd14747d..6e83f4faf 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -222,7 +222,9 @@ export default class CreatePost extends React.Component {
this.setState({uploadsInProgress: draft.uploadsInProgress, previews: draft.previews});
}
handleUploadError(err, clientId) {
- if (clientId !== -1) {
+ if (clientId === -1) {
+ this.setState({serverError: err});
+ } else {
const draft = PostStore.getDraft(this.state.channelId);
const index = draft.uploadsInProgress.indexOf(clientId);
@@ -233,8 +235,6 @@ export default class CreatePost extends React.Component {
PostStore.storeDraft(this.state.channelId, draft);
this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: err});
- } else {
- this.setState({serverError: err});
}
}
handleTextDrop(text) {
@@ -248,15 +248,15 @@ export default class CreatePost extends React.Component {
// id can either be the path of an uploaded file or the client id of an in progress upload
let index = previews.indexOf(id);
- if (index !== -1) {
- previews.splice(index, 1);
- } else {
+ if (index === -1) {
index = uploadsInProgress.indexOf(id);
if (index !== -1) {
uploadsInProgress.splice(index, 1);
this.refs.fileUpload.cancelUpload(id);
}
+ } else {
+ previews.splice(index, 1);
}
const draft = PostStore.getCurrentDraft();
diff --git a/web/react/components/delete_channel_modal.jsx b/web/react/components/delete_channel_modal.jsx
index 44c54db72..71c636921 100644
--- a/web/react/components/delete_channel_modal.jsx
+++ b/web/react/components/delete_channel_modal.jsx
@@ -11,6 +11,7 @@ export default class DeleteChannelModal extends React.Component {
super(props);
this.handleDelete = this.handleDelete.bind(this);
+ this.onShow = this.onShow.bind(this);
this.state = {
title: '',
@@ -32,14 +33,15 @@ export default class DeleteChannelModal extends React.Component {
}
);
}
+ onShow(e) {
+ var button = $(e.relatedTarget);
+ this.setState({
+ title: button.attr('data-title'),
+ channelId: button.attr('data-channelid')
+ });
+ }
componentDidMount() {
- $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function handleShow(e) {
- var button = $(e.relatedTarget);
- this.setState({
- title: button.attr('data-title'),
- channelId: button.attr('data-channelid')
- });
- }.bind(this));
+ $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
}
render() {
const channel = ChannelStore.getCurrent();
diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx
index 075f9c742..8e48a7a1c 100644
--- a/web/react/components/delete_post_modal.jsx
+++ b/web/react/components/delete_post_modal.jsx
@@ -16,6 +16,7 @@ export default class DeletePostModal extends React.Component {
this.handleDelete = this.handleDelete.bind(this);
this.onListenerChange = this.onListenerChange.bind(this);
+ this.onShow = this.onShow.bind(this);
this.state = {title: '', postId: '', channelId: '', selectedList: PostStore.getSelectedPost(), comments: 0};
}
@@ -60,18 +61,19 @@ export default class DeletePostModal extends React.Component {
}
);
}
+ onShow(e) {
+ var newState = {};
+ if (BrowserStore.getItem('edit_state_transfer')) {
+ newState = BrowserStore.getItem('edit_state_transfer');
+ BrowserStore.removeItem('edit_state_transfer');
+ } else {
+ var button = e.relatedTarget;
+ newState = {title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), postId: $(button).attr('data-postid'), comments: $(button).attr('data-comments')};
+ }
+ this.setState(newState);
+ }
componentDidMount() {
- $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function freshOpen(e) {
- var newState = {};
- if (BrowserStore.getItem('edit_state_transfer')) {
- newState = BrowserStore.getItem('edit_state_transfer');
- BrowserStore.removeItem('edit_state_transfer');
- } else {
- var button = e.relatedTarget;
- newState = {title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), postId: $(button).attr('data-postid'), comments: $(button).attr('data-comments')};
- }
- this.setState(newState);
- }.bind(this));
+ $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
PostStore.addSelectedPostChangeListener(this.onListenerChange);
}
componentWillUnmount() {
diff --git a/web/react/components/edit_channel_modal.jsx b/web/react/components/edit_channel_modal.jsx
index e93bab431..27219aba5 100644
--- a/web/react/components/edit_channel_modal.jsx
+++ b/web/react/components/edit_channel_modal.jsx
@@ -11,6 +11,7 @@ export default class EditChannelModal extends React.Component {
this.handleEdit = this.handleEdit.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
this.handleClose = this.handleClose.bind(this);
+ this.onShow = this.onShow.bind(this);
this.state = {
description: '',
@@ -50,11 +51,12 @@ export default class EditChannelModal extends React.Component {
handleClose() {
this.setState({description: '', serverError: ''});
}
+ onShow(e) {
+ const button = e.relatedTarget;
+ this.setState({description: $(button).attr('data-desc'), title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), serverError: ''});
+ }
componentDidMount() {
- $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function handleShow(e) {
- const button = e.relatedTarget;
- this.setState({description: $(button).attr('data-desc'), title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), serverError: ''});
- }.bind(this));
+ $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
$(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.handleClose);
}
componentWillUnmount() {
diff --git a/web/react/components/file_upload.jsx b/web/react/components/file_upload.jsx
index 3dc4e5de2..0e9297b7b 100644
--- a/web/react/components/file_upload.jsx
+++ b/web/react/components/file_upload.jsx
@@ -97,7 +97,7 @@ export default class FileUpload extends React.Component {
element[0].type = 'text';
element[0].type = 'file';
}
- } catch(e) {
+ } catch (e) {
// Do nothing
}
}
diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx
index 5d8b13f00..1d4aac3e6 100644
--- a/web/react/components/get_link_modal.jsx
+++ b/web/react/components/get_link_modal.jsx
@@ -8,18 +8,22 @@ export default class GetLinkModal extends React.Component {
super(props);
this.handleClick = this.handleClick.bind(this);
+ this.onShow = this.onShow.bind(this);
+ this.onHide = this.onHide.bind(this);
this.state = {copiedLink: false};
}
+ onShow(e) {
+ var button = e.relatedTarget;
+ this.setState({title: $(button).attr('data-title'), value: $(button).attr('data-value')});
+ }
+ onHide() {
+ this.setState({copiedLink: false});
+ }
componentDidMount() {
if (this.refs.modal) {
- $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function show(e) {
- var button = e.relatedTarget;
- this.setState({title: $(button).attr('data-title'), value: $(button).attr('data-value')});
- }.bind(this));
- $(React.findDOMNode(this.refs.modal)).on('hide.bs.modal', function hide() {
- this.setState({copiedLink: false});
- }.bind(this));
+ $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
+ $(React.findDOMNode(this.refs.modal)).on('hide.bs.modal', this.onShow);
}
}
handleClick() {
diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx
index 8cc4f1483..70f7a5d6e 100644
--- a/web/react/components/login.jsx
+++ b/web/react/components/login.jsx
@@ -88,10 +88,10 @@ export default class Login extends React.Component {
let focusEmail = false;
let focusPassword = false;
- if (priorEmail !== '') {
- focusPassword = true;
- } else {
+ if (priorEmail === '') {
focusEmail = true;
+ } else {
+ focusPassword = true;
}
let loginMessage = [];
diff --git a/web/react/components/mention.jsx b/web/react/components/mention.jsx
index 3a09e843d..ef7cec408 100644
--- a/web/react/components/mention.jsx
+++ b/web/react/components/mention.jsx
@@ -18,7 +18,9 @@ export default class Mention extends React.Component {
var timestamp = UserStore.getCurrentUser().update_at;
if (this.props.id === 'allmention' || this.props.id === 'channelmention') {
icon = <span><i className='mention-img fa fa-users fa-2x'></i></span>;
- } else if (this.props.id != null) {
+ } else if (this.props.id == null) {
+ icon = <span><i className='mention-img fa fa-users fa-2x'></i></span>;
+ } else {
icon = (
<span>
<img
@@ -27,8 +29,6 @@ export default class Mention extends React.Component {
/>
</span>
);
- } else {
- icon = <span><i className='mention-img fa fa-users fa-2x'></i></span>;
}
return (
<div
diff --git a/web/react/components/mention_list.jsx b/web/react/components/mention_list.jsx
index 46a9d76ae..72f51013c 100644
--- a/web/react/components/mention_list.jsx
+++ b/web/react/components/mention_list.jsx
@@ -26,53 +26,64 @@ export default class MentionList extends React.Component {
this.addFirstMention = this.addFirstMention.bind(this);
this.isEmpty = this.isEmpty.bind(this);
this.scrollToMention = this.scrollToMention.bind(this);
+ this.onScroll = this.onScroll.bind(this);
+ this.onMentionListKey = this.onMentionListKey.bind(this);
+ this.onClick = this.onClick.bind(this);
this.state = {excludeUsers: [], mentionText: '-1', selectedMention: 0, selectedUsername: ''};
}
- componentDidMount() {
- PostStore.addMentionDataChangeListener(this.onListenerChange);
+ onScroll() {
+ if ($('.mentions--top').length) {
+ $('#reply_mention_tab .mentions--top').css({bottom: $(window).height() - $('.post-right__scroll #reply_textbox').offset().top});
+ }
+ }
+ onMentionListKey(e) {
+ if (!this.isEmpty() && this.state.mentionText !== '-1' && (e.which === 13 || e.which === 9)) {
+ e.stopPropagation();
+ e.preventDefault();
+ this.addCurrentMention();
+ } else if (!this.isEmpty() && this.state.mentionText !== '-1' && (e.which === 38 || e.which === 40)) {
+ e.stopPropagation();
+ e.preventDefault();
- $('.post-right__scroll').scroll(function onScroll() {
- if ($('.mentions--top').length) {
- $('#reply_mention_tab .mentions--top').css({bottom: $(window).height() - $('.post-right__scroll #reply_textbox').offset().top});
+ if (e.which === 38) {
+ if (this.getSelection(this.state.selectedMention - 1)) {
+ this.setState({selectedMention: this.state.selectedMention - 1, selectedUsername: this.refs['mention' + (this.state.selectedMention - 1)].props.username});
+ }
+ } else if (e.which === 40) {
+ if (this.getSelection(this.state.selectedMention + 1)) {
+ this.setState({selectedMention: this.state.selectedMention + 1, selectedUsername: this.refs['mention' + (this.state.selectedMention + 1)].props.username});
+ }
}
- });
- $('body').on('keydown.mentionlist', '#' + this.props.id,
- function onMentionListKey(e) {
- if (!this.isEmpty() && this.state.mentionText !== '-1' && (e.which === 13 || e.which === 9)) {
- e.stopPropagation();
- e.preventDefault();
- this.addCurrentMention();
- } else if (!this.isEmpty() && this.state.mentionText !== '-1' && (e.which === 38 || e.which === 40)) {
- e.stopPropagation();
- e.preventDefault();
+ this.scrollToMention(e.which);
+ }
+ }
+ onClick(e) {
+ if (!($('#' + this.props.id).is(e.target) || $('#' + this.props.id).has(e.target).length ||
+ ('mentionlist' in this.refs && $(React.findDOMNode(this.refs.mentionlist)).has(e.target).length))) {
+ this.setState({mentionText: '-1'});
+ }
+ }
+ componentDidMount() {
+ PostStore.addMentionDataChangeListener(this.onListenerChange);
- if (e.which === 38) {
- if (this.getSelection(this.state.selectedMention - 1)) {
- this.setState({selectedMention: this.state.selectedMention - 1, selectedUsername: this.refs['mention' + (this.state.selectedMention - 1)].props.username});
- }
- } else if (e.which === 40) {
- if (this.getSelection(this.state.selectedMention + 1)) {
- this.setState({selectedMention: this.state.selectedMention + 1, selectedUsername: this.refs['mention' + (this.state.selectedMention + 1)].props.username});
- }
- }
+ $('.post-right__scroll').scroll(this.onScroll);
- this.scrollToMention(e.which);
- }
- }.bind(this)
- );
- $(document).click(function onClick(e) {
- if (!($('#' + this.props.id).is(e.target) || $('#' + this.props.id).has(e.target).length ||
- ('mentionlist' in this.refs && $(React.findDOMNode(this.refs.mentionlist)).has(e.target).length))) {
- this.setState({mentionText: '-1'});
- }
- }.bind(this));
+ $('body').on('keydown.mentionlist', '#' + this.props.id, this.onMentionListKey);
+ $(document).click(this.onClick);
}
componentWillUnmount() {
PostStore.removeMentionDataChangeListener(this.onListenerChange);
$('body').off('keydown.mentionlist', '#' + this.props.id);
}
+
+ /*
+ * This component is poorly designed, nessesitating some state modification
+ * in the componentDidUpdate function. This is generally discouraged as it
+ * is a performance issue and breaks with good react design. This component
+ * should be redesigned.
+ */
componentDidUpdate() {
if (this.state.mentionText !== '-1') {
if (this.state.selectedUsername !== '' && (!this.getSelection(this.state.selectedMention) || this.state.selectedUsername !== this.refs['mention' + this.state.selectedMention].props.username)) {
@@ -80,17 +91,17 @@ export default class MentionList extends React.Component {
var foundMatch = false;
while (tempSelectedMention < this.state.selectedMention && this.getSelection(++tempSelectedMention)) {
if (this.state.selectedUsername === this.refs['mention' + tempSelectedMention].props.username) {
- this.setState({selectedMention: tempSelectedMention});
+ this.setState({selectedMention: tempSelectedMention}); //eslint-disable-line react/no-did-update-set-state
foundMatch = true;
break;
}
}
if (this.getSelection(0) && !foundMatch) {
- this.setState({selectedMention: 0, selectedUsername: this.refs.mention0.props.username});
+ this.setState({selectedMention: 0, selectedUsername: this.refs.mention0.props.username}); //eslint-disable-line react/no-did-update-set-state
}
}
} else if (this.state.selectedMention !== 0) {
- this.setState({selectedMention: 0, selectedUsername: ''});
+ this.setState({selectedMention: 0, selectedUsername: ''}); //eslint-disable-line react/no-did-update-set-state
}
}
onListenerChange(id, mentionText) {
@@ -124,10 +135,10 @@ export default class MentionList extends React.Component {
return true;
}
addCurrentMention() {
- if (!this.getSelection(this.state.selectedMention)) {
- this.addFirstMention();
- } else {
+ if (this.getSelection(this.state.selectedMention)) {
this.refs['mention' + this.state.selectedMention].handleClick();
+ } else {
+ this.addFirstMention();
}
}
addFirstMention() {
diff --git a/web/react/components/more_channels.jsx b/web/react/components/more_channels.jsx
index 65cd40975..487192d91 100644
--- a/web/react/components/more_channels.jsx
+++ b/web/react/components/more_channels.jsx
@@ -60,9 +60,7 @@ export default class MoreChannels extends React.Component {
this.setState({joiningChannel: -1});
}.bind(this),
function joinFail(err) {
- this.setState({joiningChannel: -1});
- this.state.serverError = err.message;
- this.setState(this.state);
+ this.setState({joiningChannel: -1, serverError: err.message});
}.bind(this)
);
}
@@ -81,56 +79,54 @@ export default class MoreChannels extends React.Component {
if (this.state.channels != null) {
var channels = this.state.channels;
- if (!channels.loading) {
- if (channels.length) {
- moreChannels = (
- <table className='more-channel-table table'>
- <tbody>
- {channels.map(function cMap(channel, index) {
- var joinButton;
- if (self.state.joiningChannel === index) {
- joinButton = (
- <img
- className='join-channel-loading-gif'
- src='/static/images/load.gif'
- />
- );
- } else {
- joinButton = (
- <button
- onClick={self.handleJoin.bind(self, channel, index)}
- className='btn btn-primary'
- >
- Join
- </button>
- );
- }
+ if (channels.loading) {
+ moreChannels = <LoadingScreen />;
+ } else if (channels.length) {
+ moreChannels = (
+ <table className='more-channel-table table'>
+ <tbody>
+ {channels.map(function cMap(channel, index) {
+ var joinButton;
+ if (self.state.joiningChannel === index) {
+ joinButton = (
+ <img
+ className='join-channel-loading-gif'
+ src='/static/images/load.gif'
+ />
+ );
+ } else {
+ joinButton = (
+ <button
+ onClick={self.handleJoin.bind(self, channel, index)}
+ className='btn btn-primary'
+ >
+ Join
+ </button>
+ );
+ }
- return (
- <tr key={channel.id}>
- <td>
- <p className='more-channel-name'>{channel.display_name}</p>
- <p className='more-channel-description'>{channel.description}</p>
- </td>
- <td className='td--action'>
- {joinButton}
- </td>
- </tr>
- );
- })}
- </tbody>
- </table>
- );
- } else {
- moreChannels = (
- <div className='no-channel-message'>
- <p className='primary-message'>No more channels to join</p>
- <p className='secondary-message'>Click 'Create New Channel' to make a new one</p>
- </div>
- );
- }
+ return (
+ <tr key={channel.id}>
+ <td>
+ <p className='more-channel-name'>{channel.display_name}</p>
+ <p className='more-channel-description'>{channel.description}</p>
+ </td>
+ <td className='td--action'>
+ {joinButton}
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ );
} else {
- moreChannels = <LoadingScreen />;
+ moreChannels = (
+ <div className='no-channel-message'>
+ <p className='primary-message'>No more channels to join</p>
+ <p className='secondary-message'>Click 'Create New Channel' to make a new one</p>
+ </div>
+ );
}
}
diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx
index 54d77c358..c71abd43a 100644
--- a/web/react/components/more_direct_channels.jsx
+++ b/web/react/components/more_direct_channels.jsx
@@ -31,22 +31,7 @@ export default class MoreDirectChannels extends React.Component {
var active = '';
var handleClick = null;
- if (!channel.fake) {
- if (channel.id === ChannelStore.getCurrentId()) {
- active = 'active';
- }
-
- if (channel.unread) {
- badge = <span className='badge pull-right small'>{channel.unread}</span>;
- titleClass = 'unread-title';
- }
-
- handleClick = function clickHandler(e) {
- e.preventDefault();
- utils.switchChannel(channel);
- $(React.findDOMNode(self.refs.modal)).modal('hide');
- };
- } else {
+ if (channel.fake) {
// It's a direct message channel that doesn't exist yet so let's create it now
var otherUserId = utils.getUserIdFromChannelName(channel);
@@ -78,6 +63,21 @@ export default class MoreDirectChannels extends React.Component {
);
};
}
+ } else {
+ if (channel.id === ChannelStore.getCurrentId()) {
+ active = 'active';
+ }
+
+ if (channel.unread) {
+ badge = <span className='badge pull-right small'>{channel.unread}</span>;
+ titleClass = 'unread-title';
+ }
+
+ handleClick = function clickHandler(e) {
+ e.preventDefault();
+ utils.switchChannel(channel);
+ $(React.findDOMNode(self.refs.modal)).modal('hide');
+ };
}
return (
diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx
index 57a78a0d4..78057d10b 100644
--- a/web/react/components/navbar_dropdown.jsx
+++ b/web/react/components/navbar_dropdown.jsx
@@ -9,7 +9,7 @@ var TeamStore = require('../stores/team_store.jsx');
var Constants = require('../utils/constants.jsx');
function getStateFromStores() {
- return {teams: UserStore.getTeams(), currentTeam: TeamStore.getCurrent()};
+ return {teams: UserStore.getTeams()};
}
export default class NavbarDropdown extends React.Component {
@@ -142,10 +142,10 @@ export default class NavbarDropdown extends React.Component {
>
</li>
);
- if (this.state.teams.length > 1 && this.state.currentTeam) {
- var curTeamName = this.state.currentTeam.name;
+
+ if (this.state.teams.length > 1) {
this.state.teams.forEach((teamName) => {
- if (teamName !== curTeamName) {
+ if (teamName !== this.props.teamName) {
teams.push(<li key={teamName}><a href={Utils.getWindowLocationOrigin() + '/' + teamName}>{'Switch to ' + teamName}</a></li>);
}
});
@@ -234,5 +234,7 @@ NavbarDropdown.defaultProps = {
teamType: ''
};
NavbarDropdown.propTypes = {
- teamType: React.PropTypes.string
+ teamType: React.PropTypes.string,
+ teamDisplayName: React.PropTypes.string,
+ teamName: React.PropTypes.string
};
diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx
index 95a88c3d6..aaaea3c6f 100644
--- a/web/react/components/popover_list_members.jsx
+++ b/web/react/components/popover_list_members.jsx
@@ -67,9 +67,6 @@ export default class PopoverListMembers extends React.Component {
>
<div
id='member_tooltip'
- data-placement='left'
- data-toggle='tooltip'
- title='View Channel Members'
>
{countText}
<span
diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx
index 9127f00de..ac9c9252e 100644
--- a/web/react/components/post.jsx
+++ b/web/react/components/post.jsx
@@ -158,11 +158,18 @@ export default class Post extends React.Component {
var profilePic = null;
if (!this.props.hideProfilePic) {
+ let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp;
+ if (post.props && post.props.from_webhook && global.window.config.EnablePostIconOverride === 'true') {
+ if (post.props.override_icon_url) {
+ src = post.props.override_icon_url;
+ }
+ }
+
profilePic = (
<div className='post-profile-img__container'>
<img
className='post-profile-img'
- src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp}
+ src={src}
height='36'
width='36'
/>
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index 48b268700..6cfd243de 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -125,7 +125,7 @@ export default class PostBody extends React.Component {
url: 'https://www.googleapis.com/youtube/v3/videos',
type: 'GET',
data: {part: 'snippet', id: youtubeId, key: global.window.config.GoogleDeveloperKey},
- success
+ success: success.bind(this)
});
}
diff --git a/web/react/components/post_header.jsx b/web/react/components/post_header.jsx
index 9dc525e03..dd79b3e36 100644
--- a/web/react/components/post_header.jsx
+++ b/web/react/components/post_header.jsx
@@ -12,9 +12,27 @@ export default class PostHeader extends React.Component {
render() {
var post = this.props.post;
+ let userProfile = <UserProfile userId={post.user_id} />;
+ let botIndicator;
+
+ if (post.props && post.props.from_webhook) {
+ if (post.props.override_username && global.window.config.EnablePostUsernameOverride === 'true') {
+ userProfile = (
+ <UserProfile
+ userId={post.user_id}
+ overwriteName={post.props.override_username}
+ disablePopover={true}
+ />
+ );
+ }
+
+ botIndicator = <li className='post-header-col post-header__name bot-indicator'>{'BOT'}</li>;
+ }
+
return (
<ul className='post-header post-header-post'>
- <li className='post-header-col post-header__name'><strong><UserProfile userId={post.user_id} /></strong></li>
+ <li className='post-header-col post-header__name'><strong>{userProfile}</strong></li>
+ {botIndicator}
<li className='post-info--hidden'>
<PostInfo
post={post}
diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx
index a31967257..6741a9bdd 100644
--- a/web/react/components/post_list.jsx
+++ b/web/react/components/post_list.jsx
@@ -37,9 +37,11 @@ export default class PostList extends React.Component {
this.deactivate = this.deactivate.bind(this);
this.resize = this.resize.bind(this);
- this.state = this.getStateFromStores(props.channelId);
- this.state.numToDisplay = Constants.POST_CHUNK_SIZE;
- this.state.isFirstLoadComplete = false;
+ const state = this.getStateFromStores(props.channelId);
+ state.numToDisplay = Constants.POST_CHUNK_SIZE;
+ state.isFirstLoadComplete = false;
+
+ this.state = state;
}
getStateFromStores(id) {
var postList = PostStore.getPosts(id);
@@ -449,10 +451,10 @@ export default class PostList extends React.Component {
}
var createMessage;
- if (creatorName !== '') {
- createMessage = (<span>This is the start of the <strong>{uiName}</strong> {uiType}, created by <strong>{creatorName}</strong> on <strong>{utils.displayDate(channel.create_at)}</strong></span>);
- } else {
+ if (creatorName === '') {
createMessage = 'This is the start of the ' + uiName + ' ' + uiType + ', created on ' + utils.displayDate(channel.create_at) + '.';
+ } else {
+ createMessage = (<span>This is the start of the <strong>{uiName}</strong> {uiType}, created by <strong>{creatorName}</strong> on <strong>{utils.displayDate(channel.create_at)}</strong></span>);
}
return (
@@ -516,8 +518,19 @@ export default class PostList extends React.Component {
sameRoot = utils.isComment(post) && (prevPost.id === post.root_id || prevPost.root_id === post.root_id);
- // we only hide the profile pic if the previous post is not a comment, the current post is not a comment, and the previous post was made by the same user as the current post
- hideProfilePic = (prevPost.user_id === post.user_id) && !utils.isComment(prevPost) && !utils.isComment(post);
+ // hide the profile pic if:
+ // the previous post was made by the same user as the current post,
+ // the previous post is not a comment,
+ // the current post is not a comment,
+ // the current post is not from a webhook
+ // and the previous post is not from a webhook
+ if ((prevPost.user_id === post.user_id) &&
+ !utils.isComment(prevPost) &&
+ !utils.isComment(post) &&
+ (!post.props || !post.props.from_webhook) &&
+ (!prevPost.props || !prevPost.props.from_webhook)) {
+ hideProfilePic = true;
+ }
}
// check if it's the last comment in a consecutive string of comments on the same post
diff --git a/web/react/components/rename_channel_modal.jsx b/web/react/components/rename_channel_modal.jsx
index 9d514c741..d60206ecf 100644
--- a/web/react/components/rename_channel_modal.jsx
+++ b/web/react/components/rename_channel_modal.jsx
@@ -15,6 +15,7 @@ export default class RenameChannelModal extends React.Component {
this.onDisplayNameChange = this.onDisplayNameChange.bind(this);
this.displayNameKeyUp = this.displayNameKeyUp.bind(this);
this.handleClose = this.handleClose.bind(this);
+ this.handleShow = this.handleShow.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.state = {
@@ -59,11 +60,11 @@ export default class RenameChannelModal extends React.Component {
state.invalid = true;
} else {
let cleanedName = Utils.cleanUpUrlable(channel.name);
- if (cleanedName !== channel.name) {
+ if (cleanedName === channel.name) {
+ state.nameError = '';
+ } else {
state.nameError = 'Must be lowercase alphanumeric characters';
state.invalid = true;
- } else {
- state.nameError = '';
}
}
@@ -103,7 +104,7 @@ export default class RenameChannelModal extends React.Component {
this.setState({channelName: channelName});
}
handleClose() {
- this.state = {
+ this.setState({
displayName: '',
channelName: '',
channelId: '',
@@ -111,13 +112,14 @@ export default class RenameChannelModal extends React.Component {
nameError: '',
displayNameError: '',
invalid: false
- };
+ });
+ }
+ handleShow(e) {
+ const button = $(e.relatedTarget);
+ this.setState({displayName: button.attr('data-display'), channelName: button.attr('data-name'), channelId: button.attr('data-channelid')});
}
componentDidMount() {
- $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function handleShow(e) {
- const button = $(e.relatedTarget);
- this.setState({displayName: button.attr('data-display'), channelName: button.attr('data-name'), channelId: button.attr('data-channelid')});
- }.bind(this));
+ $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', this.handleShow);
$(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.handleClose);
}
componentWillUnmount() {
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index 6d4b56b7b..b696f4b53 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -29,9 +29,11 @@ export default class Sidebar extends React.Component {
this.updateUnreadIndicators = this.updateUnreadIndicators.bind(this);
this.createChannelElement = this.createChannelElement.bind(this);
- this.state = this.getStateFromStores();
- this.state.modal = '';
- this.state.loadingDMChannel = -1;
+ const state = this.getStateFromStores();
+ state.modal = '';
+ state.loadingDMChannel = -1;
+
+ this.state = state;
}
getStateFromStores() {
var members = ChannelStore.getAllMembers();
@@ -65,7 +67,18 @@ export default class Sidebar extends React.Component {
var channel = ChannelStore.getByName(channelName);
- if (channel != null) {
+ if (channel == null) {
+ var tempChannel = {};
+ tempChannel.fake = true;
+ tempChannel.name = channelName;
+ tempChannel.display_name = teammate.username;
+ tempChannel.teammate_username = teammate.username;
+ tempChannel.status = UserStore.getStatus(teammate.id);
+ tempChannel.last_post_at = 0;
+ tempChannel.total_msg_count = 0;
+ tempChannel.type = 'D';
+ readDirectChannels.push(tempChannel);
+ } else {
channel.display_name = teammate.username;
channel.teammate_username = teammate.username;
@@ -80,17 +93,6 @@ export default class Sidebar extends React.Component {
} else {
readDirectChannels.push(channel);
}
- } else {
- var tempChannel = {};
- tempChannel.fake = true;
- tempChannel.name = channelName;
- tempChannel.display_name = teammate.username;
- tempChannel.teammate_username = teammate.username;
- tempChannel.status = UserStore.getStatus(teammate.id);
- tempChannel.last_post_at = 0;
- tempChannel.total_msg_count = 0;
- tempChannel.type = 'D';
- readDirectChannels.push(tempChannel);
}
}
@@ -512,6 +514,7 @@ export default class Sidebar extends React.Component {
/>
<SidebarHeader
teamDisplayName={this.props.teamDisplayName}
+ teamName={this.props.teamName}
teamType={this.props.teamType}
/>
<SearchBox />
@@ -591,5 +594,6 @@ Sidebar.defaultProps = {
};
Sidebar.propTypes = {
teamType: React.PropTypes.string,
- teamDisplayName: React.PropTypes.string
+ teamDisplayName: React.PropTypes.string,
+ teamName: React.PropTypes.string
};
diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx
index 072c14e0a..33de35064 100644
--- a/web/react/components/sidebar_header.jsx
+++ b/web/react/components/sidebar_header.jsx
@@ -52,6 +52,8 @@ export default class SidebarHeader extends React.Component {
<NavbarDropdown
ref='dropdown'
teamType={this.props.teamType}
+ teamDisplayName={this.props.teamDisplayName}
+ teamName={this.props.teamName}
/>
</div>
);
@@ -64,5 +66,6 @@ SidebarHeader.defaultProps = {
};
SidebarHeader.propTypes = {
teamDisplayName: React.PropTypes.string,
+ teamName: React.PropTypes.string,
teamType: React.PropTypes.string
};
diff --git a/web/react/components/sidebar_right.jsx b/web/react/components/sidebar_right.jsx
index 708cd04cb..573515a46 100644
--- a/web/react/components/sidebar_right.jsx
+++ b/web/react/components/sidebar_right.jsx
@@ -30,11 +30,11 @@ export default class SidebarRight extends React.Component {
PostStore.removeSelectedPostChangeListener(this.onSelectedChange);
}
componentDidUpdate() {
- if (!this.plScrolledToBottom) {
- $('.top-visible-post')[0].scrollIntoView();
- } else {
+ if (this.plScrolledToBottom) {
var postHolder = $('.post-list-holder-by-time').not('.inactive');
postHolder.scrollTop(postHolder[0].scrollHeight);
+ } else {
+ $('.top-visible-post')[0].scrollIntoView();
}
}
onSelectedChange(fromSearch) {
diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx
index 495159efc..ae3075495 100644
--- a/web/react/components/signup_user_complete.jsx
+++ b/web/react/components/signup_user_complete.jsx
@@ -41,15 +41,13 @@ export default class SignupUserComplete extends React.Component {
return;
}
- this.state.user.email = providedEmail;
-
- this.state.user.username = React.findDOMNode(this.refs.name).value.trim().toLowerCase();
- if (!this.state.user.username) {
+ const providedUsername = React.findDOMNode(this.refs.name).value.trim().toLowerCase();
+ if (!providedUsername) {
this.setState({nameError: 'This field is required', emailError: '', passwordError: '', serverError: ''});
return;
}
- var usernameError = Utils.isValidUsername(this.state.user.username);
+ const usernameError = Utils.isValidUsername(this.state.user.username);
if (usernameError === 'Cannot use a reserved word as a username.') {
this.setState({nameError: 'This username is reserved, please choose a new one.', emailError: '', passwordError: '', serverError: ''});
return;
@@ -63,15 +61,24 @@ export default class SignupUserComplete extends React.Component {
return;
}
- this.state.user.password = React.findDOMNode(this.refs.password).value.trim();
- if (!this.state.user.password || this.state.user.password .length < 5) {
+ const providedPassword = React.findDOMNode(this.refs.password).value.trim();
+ if (!providedPassword || providedPassword.length < 5) {
this.setState({nameError: '', emailError: '', passwordError: 'Please enter at least 5 characters', serverError: ''});
return;
}
- this.setState({nameError: '', emailError: '', passwordError: '', serverError: ''});
-
- this.state.user.allow_marketing = true;
+ this.setState({
+ user: {
+ email: providedEmail,
+ username: providedUsername,
+ password: providedPassword,
+ allow_marketing: true
+ },
+ nameError: '',
+ emailError: '',
+ passwordError: '',
+ serverError: ''
+ });
client.createUser(this.state.user, this.state.data, this.state.hash,
function createUserSuccess() {
diff --git a/web/react/components/team_signup_email_item.jsx b/web/react/components/team_signup_email_item.jsx
index 10bb2d69e..01330a46c 100644
--- a/web/react/components/team_signup_email_item.jsx
+++ b/web/react/components/team_signup_email_item.jsx
@@ -23,17 +23,14 @@ export default class TeamSignupEmailItem extends React.Component {
}
if (!Utils.isEmail(email)) {
- this.state.emailError = 'Please enter a valid email address';
- this.setState(this.state);
+ this.setState({emailError: 'Please enter a valid email address'});
return false;
} else if (email === teamEmail) {
- this.state.emailError = 'Please use a different email than the one used at signup';
- this.setState(this.state);
+ this.setState({emailError: 'Please use a different email than the one used at signup'});
return false;
}
- this.state.emailError = '';
- this.setState(this.state);
+ this.setState({emailError: ''});
return true;
}
render() {
diff --git a/web/react/components/team_signup_send_invites_page.jsx b/web/react/components/team_signup_send_invites_page.jsx
index 524bd5b50..8d8fb92ff 100644
--- a/web/react/components/team_signup_send_invites_page.jsx
+++ b/web/react/components/team_signup_send_invites_page.jsx
@@ -36,10 +36,10 @@ export default class TeamSignupSendInvitesPage extends React.Component {
var emails = [];
for (var i = 0; i < this.props.state.invites.length; i++) {
- if (!this.refs['email_' + i].validate(this.props.state.team.email)) {
- valid = false;
- } else {
+ if (this.refs['email_' + i].validate(this.props.state.team.email)) {
emails.push(this.refs['email_' + i].getValue());
+ } else {
+ valid = false;
}
}
diff --git a/web/react/components/team_signup_url_page.jsx b/web/react/components/team_signup_url_page.jsx
index a3f89a217..a682bb49e 100644
--- a/web/react/components/team_signup_url_page.jsx
+++ b/web/react/components/team_signup_url_page.jsx
@@ -48,22 +48,20 @@ export default class TeamSignupUrlPage extends React.Component {
}
Client.findTeamByName(name,
- function success(data) {
- if (!data) {
- this.props.state.wizard = 'send_invites';
- this.props.state.team.type = 'O';
-
- this.props.state.team.name = name;
- this.props.updateParent(this.props.state);
- } else {
- this.state.nameError = 'This URL is unavailable. Please try another.';
- this.setState(this.state);
- }
- }.bind(this),
- function error(err) {
- this.state.nameError = err.message;
- this.setState(this.state);
- }.bind(this)
+ (data) => {
+ if (data) {
+ this.setState({nameError: 'This URL is unavailable. Please try another.'});
+ } else {
+ this.props.state.wizard = 'send_invites';
+ this.props.state.team.type = 'O';
+
+ this.props.state.team.name = name;
+ this.props.updateParent(this.props.state);
+ }
+ },
+ (err) => {
+ this.setState({nameError: err.message});
+ }
);
}
handleFocus(e) {
diff --git a/web/react/components/team_signup_welcome_page.jsx b/web/react/components/team_signup_welcome_page.jsx
index 626c6a17b..019456c9f 100644
--- a/web/react/components/team_signup_welcome_page.jsx
+++ b/web/react/components/team_signup_welcome_page.jsx
@@ -59,8 +59,7 @@ export default class TeamSignupWelcomePage extends React.Component {
}
}.bind(this),
function error(err) {
- this.state.serverError = err.message;
- this.setState(this.state);
+ this.setState({serverError: err.message});
}.bind(this)
);
}
diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx
index c5d028d31..ceb8f52a7 100644
--- a/web/react/components/user_profile.jsx
+++ b/web/react/components/user_profile.jsx
@@ -31,8 +31,10 @@ export default class UserProfile extends React.Component {
}
componentDidMount() {
UserStore.addChangeListener(this.onChange);
- $('#profile_' + this.uniqueId).popover({placement: 'right', container: 'body', trigger: 'hover', html: true, delay: {show: 200, hide: 100}});
- $('body').tooltip({selector: '[data-toggle=tooltip]', trigger: 'hover click'});
+ if (!this.props.disablePopover) {
+ $('#profile_' + this.uniqueId).popover({placement: 'right', container: 'body', trigger: 'hover', html: true, delay: {show: 200, hide: 100}});
+ $('body').tooltip({selector: '[data-toggle=tooltip]', trigger: 'hover click'});
+ }
}
componentWillUnmount() {
UserStore.removeChangeListener(this.onChange);
@@ -56,6 +58,10 @@ export default class UserProfile extends React.Component {
name = this.props.overwriteName;
}
+ if (this.props.disablePopover) {
+ return <div>{name}</div>;
+ }
+
var dataContent = '<img class="user-popover__image" src="/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at + '" height="128" width="128" />';
if (!global.window.config.ShowEmailAddress === 'true') {
dataContent += '<div class="text-nowrap">Email not shared</div>';
@@ -79,9 +85,11 @@ export default class UserProfile extends React.Component {
UserProfile.defaultProps = {
userId: '',
- overwriteName: ''
+ overwriteName: '',
+ disablePopover: false
};
UserProfile.propTypes = {
userId: React.PropTypes.string,
- overwriteName: React.PropTypes.string
+ overwriteName: React.PropTypes.string,
+ disablePopover: React.PropTypes.bool
};
diff --git a/web/react/components/user_settings/import_theme_modal.jsx b/web/react/components/user_settings/import_theme_modal.jsx
index 0f5cb59f5..4e8ee03fa 100644
--- a/web/react/components/user_settings/import_theme_modal.jsx
+++ b/web/react/components/user_settings/import_theme_modal.jsx
@@ -150,7 +150,9 @@ export default class ImportThemeModal extends React.Component {
className='form-control'
onChange={this.handleChange}
/>
- {this.state.inputError}
+ <div className='input__help'>
+ {this.state.inputError}
+ </div>
</div>
</div>
</Modal.Body>
diff --git a/web/react/components/user_settings/premade_theme_chooser.jsx b/web/react/components/user_settings/premade_theme_chooser.jsx
index f8f916bd0..8116bffcc 100644
--- a/web/react/components/user_settings/premade_theme_chooser.jsx
+++ b/web/react/components/user_settings/premade_theme_chooser.jsx
@@ -24,7 +24,7 @@ export default class PremadeThemeChooser extends React.Component {
premadeThemes.push(
<div
- className='col-sm-3 premade-themes'
+ className='col-xs-6 col-sm-3 premade-themes'
key={'premade-theme-key' + k}
>
<div
diff --git a/web/react/package.json b/web/react/package.json
index a9eba6c6c..31295873b 100644
--- a/web/react/package.json
+++ b/web/react/package.json
@@ -19,8 +19,8 @@
"babelify": "6.1.3",
"uglify-js": "2.4.24",
"watchify": "3.3.1",
- "eslint": "1.3.1",
- "eslint-plugin-react": "3.3.1"
+ "eslint": "1.6.0",
+ "eslint-plugin-react": "3.5.1"
},
"scripts": {
"start": "watchify --extension=jsx -o ../static/js/bundle.js -v -d ./**/*.jsx",
diff --git a/web/react/pages/authorize.jsx b/web/react/pages/authorize.jsx
index db42c8266..8ea8b13eb 100644
--- a/web/react/pages/authorize.jsx
+++ b/web/react/pages/authorize.jsx
@@ -3,16 +3,16 @@
var Authorize = require('../components/authorize.jsx');
-function setupAuthorizePage(teamName, appName, responseType, clientId, redirectUri, scope, state) {
+function setupAuthorizePage(props) {
React.render(
<Authorize
- teamName={teamName}
- appName={appName}
- responseType={responseType}
- clientId={clientId}
- redirectUri={redirectUri}
- scope={scope}
- state={state}
+ teamName={props.TeamName}
+ appName={props.AppName}
+ responseType={props.ResponseType}
+ clientId={props.ClientId}
+ redirectUri={props.RedirectUri}
+ scope={props.Scope}
+ state={props.State}
/>,
document.getElementById('authorize')
);
diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx
index 74259194a..c333fd57d 100644
--- a/web/react/pages/channel.jsx
+++ b/web/react/pages/channel.jsx
@@ -36,11 +36,14 @@ var RemovedFromChannelModal = require('../components/removed_from_channel_modal.
var FileUploadOverlay = require('../components/file_upload_overlay.jsx');
var RegisterAppModal = require('../components/register_app_modal.jsx');
var ImportThemeModal = require('../components/user_settings/import_theme_modal.jsx');
+var TeamStore = require('../stores/team_store.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
function setupChannelPage(props) {
+ TeamStore.setCurrentId(props.TeamId);
+
AppDispatcher.handleViewAction({
type: ActionTypes.CLICK_CHANNEL,
name: props.ChannelName,
@@ -71,6 +74,7 @@ function setupChannelPage(props) {
React.render(
<Sidebar
teamDisplayName={props.TeamDisplayName}
+ teamName={props.TeamName}
teamType={props.TeamType}
/>,
document.getElementById('sidebar-left')
diff --git a/web/react/stores/browser_store.jsx b/web/react/stores/browser_store.jsx
index d2dedb271..e45d3d981 100644
--- a/web/react/stores/browser_store.jsx
+++ b/web/react/stores/browser_store.jsx
@@ -4,7 +4,7 @@
var UserStore;
function getPrefix() {
if (!UserStore) {
- UserStore = require('./user_store.jsx');
+ UserStore = require('./user_store.jsx'); //eslint-disable-line global-require
}
return UserStore.getCurrentId() + '_';
}
diff --git a/web/react/stores/team_store.jsx b/web/react/stores/team_store.jsx
index 1f33fe03b..fd9117747 100644
--- a/web/react/stores/team_store.jsx
+++ b/web/react/stores/team_store.jsx
@@ -10,12 +10,12 @@ var BrowserStore = require('../stores/browser_store.jsx');
var CHANGE_EVENT = 'change';
-var utils;
+var Utils;
function getWindowLocationOrigin() {
- if (!utils) {
- utils = require('../utils/utils.jsx');
+ if (!Utils) {
+ Utils = require('../utils/utils.jsx'); //eslint-disable-line global-require
}
- return utils.getWindowLocationOrigin();
+ return Utils.getWindowLocationOrigin();
}
class TeamStoreClass extends EventEmitter {
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index 5cb165b4c..d9f486009 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -15,7 +15,7 @@ function handleError(methodName, xhr, status, err) {
var e = null;
try {
e = JSON.parse(xhr.responseText);
- } catch(parseError) {
+ } catch (parseError) {
e = null;
}
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index da59f8e5a..67414dc3b 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -179,7 +179,7 @@ module.exports = {
centerChannelColor: '#DDDDDD',
newMessageSeparator: '#5de5da',
linkColor: '#A4FFEB',
- buttonBg: '#1dacfc',
+ buttonBg: '#4CBBA4',
buttonColor: '#FFFFFF'
},
windows10: {
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 8b20e2adf..307a311ab 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -231,10 +231,10 @@ function testUrlMatch(text) {
var matchText = match.getMatchedText();
linkData.text = matchText;
- if (matchText.trim().indexOf('http') !== 0) {
- linkData.link = 'http://' + matchText;
- } else {
+ if (matchText.trim().indexOf('http') === 0) {
linkData.link = matchText;
+ } else {
+ linkData.link = 'http://' + matchText;
}
result.push(linkData);
@@ -395,35 +395,39 @@ export function toTitleCase(str) {
export function applyTheme(theme) {
if (theme.sidebarBg) {
- changeCss('.sidebar--left', 'background:' + theme.sidebarBg, 1);
+ changeCss('.sidebar--left, .settings-modal .settings-table .settings-links, .sidebar--menu', 'background:' + theme.sidebarBg, 1);
}
if (theme.sidebarText) {
- changeCss('.sidebar--left .nav li>a, .sidebar--right', 'color:' + theme.sidebarText, 1);
- changeCss('.sidebar--left .nav li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.8), 1);
+ changeCss('.sidebar--left .nav-pills__container li>a, .sidebar--right, .settings-modal .nav-pills>li a, .sidebar--menu', 'color:' + theme.sidebarText, 1);
+ changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li>a', 'color:' + theme.sidebarText, 1);
+ changeCss('.sidebar--left .nav-pills__container 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--left .status path', 'fill:' + changeOpacity(theme.sidebarText, 0.5), 1);
+ changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li>a', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 2);
}
if (theme.sidebarUnreadText) {
- changeCss('.sidebar--left .nav li>a.unread-title', 'color:' + theme.sidebarUnreadText + '!important;', 1);
+ changeCss('.sidebar--left .nav-pills__container li>a.unread-title', 'color:' + theme.sidebarUnreadText + '!important;', 2);
}
if (theme.sidebarTextHoverBg) {
- changeCss('.sidebar--left .nav li>a:hover, .sidebar--left .nav li>a:focus', 'background:' + theme.sidebarTextHoverBg, 1);
+ changeCss('.sidebar--left .nav-pills__container li>a:hover, .sidebar--left .nav-pills__container li>a:focus, .settings-modal .nav-pills>li:hover a, .settings-modal .nav-pills>li:focus a', 'background:' + theme.sidebarTextHoverBg, 1);
+ changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li:hover a', 'background:' + theme.sidebarTextHoverBg, 1);
}
if (theme.sidebarTextHoverColor) {
- changeCss('.sidebar--left .nav li>a:hover, .sidebar--left .nav li>a:focus', 'color:' + theme.sidebarTextHoverColor, 2);
+ changeCss('.sidebar--left .nav-pills__container li>a:hover, .sidebar--left .nav-pills__container li>a:focus, .settings-modal .nav-pills>li:hover a, .settings-modal .nav-pills>li:focus a', 'color:' + theme.sidebarTextHoverColor, 2);
+ changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li:hover a', 'color:' + theme.sidebarTextHoverColor, 2);
}
if (theme.sidebarTextActiveBg) {
- changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'background:' + theme.sidebarTextActiveBg, 1);
+ changeCss('.sidebar--left .nav-pills__container li.active a, .sidebar--left .nav-pills__container li.active a:hover, .sidebar--left .nav-pills__container li.active a:focus, .settings-modal .nav-pills>li.active a, .settings-modal .nav-pills>li.active a:hover, .settings-modal .nav-pills>li.active a:active', 'background:' + theme.sidebarTextActiveBg, 1);
}
if (theme.sidebarTextActiveColor) {
- changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'color:' + theme.sidebarTextActiveColor, 2);
+ changeCss('.sidebar--left .nav-pills__container li.active a, .sidebar--left .nav-pills__container li.active a:hover, .sidebar--left .nav-pills__container li.active a:focus, .settings-modal .nav-pills>li.active a, .settings-modal .nav-pills>li.active a:hover, .settings-modal .nav-pills>li.active a:active', 'color:' + theme.sidebarTextActiveColor, 2);
}
if (theme.sidebarHeaderBg) {
@@ -458,45 +462,51 @@ export function applyTheme(theme) {
}
if (theme.centerChannelBg) {
- changeCss('.app__content, .markdown__table, .markdown__table tbody tr, .command-box', 'background:' + theme.centerChannelBg, 1);
+ changeCss('.app__content, .markdown__table, .markdown__table tbody tr, .command-box, .modal .modal-content, .mentions-name, .mentions--top .mentions-box', 'background:' + theme.centerChannelBg, 1);
changeCss('#post-list .post-list-holder-by-time', 'background:' + theme.centerChannelBg, 1);
changeCss('#post-create', '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);
+ changeCss('.sidebar--right, .dropdown-menu, .popover', 'background:' + theme.centerChannelBg, 1);
}
if (theme.centerChannelColor) {
- changeCss('.app__content, .post-create__container .post-create-body .btn-file, .post-create__container .post-create-footer .msg-typing, .loading-screen .loading__content .round, .command-name', 'color:' + theme.centerChannelColor, 1);
+ changeCss('.app__content, .post-create__container .post-create-body .btn-file, .post-create__container .post-create-footer .msg-typing, .command-name, .modal .modal-content, .dropdown-menu, .popover, .mentions-name', 'color:' + theme.centerChannelColor, 1);
changeCss('#post-create', 'color:' + theme.centerChannelColor, 2);
changeCss('.mentions--top, .command-box', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 3);
changeCss('.mentions--top, .command-box', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 2);
changeCss('.mentions--top, .command-box', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 1);
- changeCss('.post-body hr', 'background:' + theme.centerChannelColor, 1);
+ changeCss('.dropdown-menu, .popover ', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 3);
+ changeCss('.dropdown-menu, .popover ', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 2);
+ changeCss('.dropdown-menu, .popover ', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 1);
+ changeCss('.post-body hr, .loading-screen .loading__content .round', '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, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td, .command-box', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
- changeCss('.command-name', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 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, .command-box, .modal .modal-content, .settings-modal .settings-table .settings-content .divider-light, .dropdown-menu, .modal .modal-header, .popover, .mentions--top .mentions-box', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.command-name, .popover .popover-title', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.dropdown-menu .divider', 'background:' + theme.centerChannelColor, 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('@media(max-width: 768px){.search-bar__container .search__form .search-bar', 'background:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.search-bar__container .search__form .search-bar', 'background: transparent; color:' + theme.centerChannelColor, 1);
+ changeCss('@media(max-width: 768px){.search-bar__container .search__form .search-bar', 'background:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: inherit;', 1);
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', '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('.date-separator .separator__hr, .modal-footer, .modal .custom-textarea, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.modal .custom-textarea:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
+ changeCss('.channel-intro, .settings-modal .settings-table .settings-content .divider-dark, hr, .settings-modal .settings-table .settings-links', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, .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);
- changeCss('.date-separator.hovered--before:after, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
- changeCss('.date-separator.hovered--after:before, .new-separator.hovered--after:before, .command-name:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.post:hover, .sidebar--right .sidebar--right__header, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.date-separator.hovered--before:after, .date-separator.hovered--after:before, .new-separator.hovered--after:before, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.command-name:hover, .mentions-name:hover, .mentions-focus, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
changeCss('.post.current--user:hover .post-body ', 'background: none;', 1);
changeCss('.sidebar--right', 'color:' + theme.centerChannelColor, 2);
}
@@ -507,7 +517,7 @@ export function applyTheme(theme) {
}
if (theme.linkColor) {
- changeCss('a, a:focus, a:hover', 'color:' + theme.linkColor, 1);
+ changeCss('a, a:focus, a:hover, .btn, .btn:focus, .btn:hover', 'color:' + theme.linkColor, 1);
changeCss('.post .comment-icon__container', 'fill:' + theme.linkColor, 1);
}
@@ -630,7 +640,7 @@ export function isValidUsername(name) {
error = 'Must be between 3 and 15 characters';
} else if (!(/^[a-z0-9\.\-\_]+$/).test(name)) {
error = "Must contain only letters, numbers, and the symbols '.', '-', and '_'.";
- } else if (!(/[a-z]/).test(name.charAt(0))) {
+ } else if (!(/[a-z]/).test(name.charAt(0))) { //eslint-disable-line no-negated-condition
error = 'First character must be a letter.';
} else {
for (var i = 0; i < Constants.RESERVED_USERNAMES.length; i++) {
diff --git a/web/sass-files/sass/partials/_admin-console.scss b/web/sass-files/sass/partials/_admin-console.scss
index 11794a269..5037da415 100644
--- a/web/sass-files/sass/partials/_admin-console.scss
+++ b/web/sass-files/sass/partials/_admin-console.scss
@@ -144,7 +144,6 @@
}
.popover {
border-radius: 3px;
- border: 1px solid #ccc;
width: 100%;
font-size: 0.95em;
}
diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss
index 87d9b8200..841db7c5a 100644
--- a/web/sass-files/sass/partials/_base.scss
+++ b/web/sass-files/sass/partials/_base.scss
@@ -36,16 +36,30 @@ body {
.popover {
color: #333;
- a {
- color: $primary-color;
- &:hover, &:focus {
- color: $primary-color;
+ &.bottom {
+ >.arrow:after {
+ border-bottom-color: rgba(white, 0.5);
+ }
+ }
+ .popover-title {
+ background: rgba(black, 0.05);
+ }
+}
+
+.dropdown-menu {
+ .divider {
+ @include opacity(0.15);
+ }
+ > li > a {
+ color: inherit;
+ &:focus, &:hover {
+ color: inherit;
}
}
}
.word-break--all {
- word-break: break-all;
+ word-break: break-all;
}
a {
@@ -68,6 +82,19 @@ a:focus, a:hover {
margin: 0;
}
+.text-danger {
+ color: #E05F5D;
+}
+
+.btn {
+ &.btn-danger {
+ color: #fff;
+ &:hover, &:active {
+ color: #fff;
+ }
+ }
+}
+
.form-control {
@include border-radius(2px);
&.no-resize {
@@ -137,9 +164,9 @@ a:focus, a:hover {
}
.emoji {
- width: 1.5em;
- height: 1.5em;
- display: inline-block;
- margin-bottom: 0.25em;
- background-size: contain;
+ width: 1.5em;
+ height: 1.5em;
+ display: inline-block;
+ margin-bottom: 0.25em;
+ background-size: contain;
}
diff --git a/web/sass-files/sass/partials/_forms.scss b/web/sass-files/sass/partials/_forms.scss
index 65ea161d4..6c1f7cc6c 100644
--- a/web/sass-files/sass/partials/_forms.scss
+++ b/web/sass-files/sass/partials/_forms.scss
@@ -16,11 +16,12 @@
}
}
.input__help {
- color: #777;
+ color: inherit;
margin: 10px 0 0 10px;
word-break: break-word;
+ @include opacity(0.8);
&.dark {
- color: #222;
+ @include opacity(1);
}
&.error {
color: #a94442;
diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss
index b5fcb6145..2a4e82fc7 100644
--- a/web/sass-files/sass/partials/_headers.scss
+++ b/web/sass-files/sass/partials/_headers.scss
@@ -37,7 +37,7 @@
&.description {
overflow: hidden;
text-overflow: ellipsis;
- color: #888;
+ @include opacity(0.6);
margin-top: 2px;
max-height: 45px;
}
@@ -114,23 +114,6 @@
}
}
}
- &.theme--black {
- &:hover {
- &:before {
- background: rgba(white, 0.2);
- }
- }
- }
- &.theme--gray {
- &:hover {
- &:before {
- background: rgba(white, 0.1);
- }
- }
- }
- a {
- color: #fff;
- }
.navbar-right {
font-size: 0.85em;
position: absolute;
@@ -145,7 +128,6 @@
.dropdown-menu {
li a {
padding: 3px 20px;
- color: #555;
text-overflow: ellipsis;
overflow: hidden;
}
diff --git a/web/sass-files/sass/partials/_mentions.scss b/web/sass-files/sass/partials/_mentions.scss
index 48746ba01..a2bd0dcea 100644
--- a/web/sass-files/sass/partials/_mentions.scss
+++ b/web/sass-files/sass/partials/_mentions.scss
@@ -33,13 +33,6 @@
line-height: 36px;
font-size: 13px;
cursor: pointer;
- &:hover {
- background-color: #E6F2FA;
- }
-}
-
-.mentions-focus {
- background-color: #E6F2FA;
}
.mentions-text {
@@ -54,7 +47,6 @@
display: block;
font-size: 20px;
text-align: center;
- color: #555;
@include border-radius(32px);
}
@@ -63,10 +55,6 @@
padding-left: 10px;
}
-.mention-link {
- color: $primary-color;
-}
-
.mention-highlight {
background-color:#fff2bb;
color: #333;
diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss
index e4e8b20b6..96b26f251 100644
--- a/web/sass-files/sass/partials/_modal.scss
+++ b/web/sass-files/sass/partials/_modal.scss
@@ -9,12 +9,6 @@
@include opacity(0.7);
}
}
- a, a:focus, a:hover {
- color: #2389D7;
- &.text-danger {
- color: #a94442;
- }
- }
.custom-textarea {
color: inherit;
border-color: #ccc;
@@ -25,12 +19,9 @@
}
.btn {
font-size: 13px;
- &.btn-primary {
- background: #4285f4;
- &:hover, &:focus, &:active {
- background: $primary-color--hover;
- color: #fff;
- }
+ &.btn-default {
+ border: none;
+ background: transparent;
}
}
.info__label {
@@ -70,7 +61,7 @@
background: $primary-color;
color: #FFF;
padding: 15px 15px 11px;
- border: none;
+ border: 1px solid #ddd;
min-height: 56px;
@include clearfix;
.modal-title {
diff --git a/web/sass-files/sass/partials/_oauth.scss b/web/sass-files/sass/partials/_oauth.scss
new file mode 100644
index 000000000..35d3a95e3
--- /dev/null
+++ b/web/sass-files/sass/partials/_oauth.scss
@@ -0,0 +1,31 @@
+.oauth-prompt {
+ background: #fff;
+ border: 1px solid #ddd;
+ padding: 1em 2em 0;
+ margin: 50px auto;
+ max-width: 90%;
+ width: 600px;
+ .prompt__heading {
+ font-size: em(20px);
+ line-height: normal;
+ margin: 1em 0;
+ display: table;
+ width: 100%;
+ > div {
+ display: table-cell;
+ vertical-align: top;
+ }
+ img {
+ margin-right: 15px;
+ }
+ }
+ .prompt__allow {
+ margin: 1em 0;
+ font-size: em(24px);
+ }
+ .prompt__buttons {
+ text-align: right;
+ border-top: 1px solid #ddd;
+ padding: 1.5em 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 7532875d6..8bf4b0534 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -509,3 +509,11 @@ body.ios {
}
}
}
+
+.bot-indicator {
+ background-color: lightgrey;
+ border-radius:2px;
+ padding-left:2px;
+ padding-right:2px;
+ font-family:"Courier New"
+}
diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss
index 9e8d0dc7d..82ec1811a 100644
--- a/web/sass-files/sass/partials/_responsive.scss
+++ b/web/sass-files/sass/partials/_responsive.scss
@@ -402,12 +402,6 @@
margin-left: 7px;
}
}
- &.active, &:hover {
- a {
- color: #555;
- background: #fff;
- }
- }
}
}
}
diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss
index 3aab05d70..42475efd5 100644
--- a/web/sass-files/sass/partials/_settings.scss
+++ b/web/sass-files/sass/partials/_settings.scss
@@ -2,7 +2,6 @@
@import "activity-log";
.user-settings {
- background: #fff;
min-height:300px;
.table-responsive {
max-width: 560px;
@@ -71,7 +70,7 @@
}
.section-max {
- background: #f2f2f2;
+ background: rgba(black, 0.05);
padding: 1em 0 1.3em;
margin-bottom: 0;
@include clearfix;
@@ -82,6 +81,7 @@
.appearance-section {
.premade-themes {
+ margin-bottom: 10px;
.theme-label {
font-weight: 400;
margin-top: 5px;
@@ -121,7 +121,7 @@
.fa {
margin-right: 7px;
font-size: 12px;
- color: #aaa;
+ @include opacity(0.5);
display: none;
}
}
@@ -131,7 +131,7 @@
}
.section-describe {
- color:grey;
+ @include opacity(0.7);
}
.divider-dark {
@@ -167,15 +167,11 @@
}
}
.control-label {
- color: #555;
font-weight: 600;
&.text-left {
text-align: left;
}
}
- hr {
- border-color: #ccc;
- }
}
.file-status {
diff --git a/web/sass-files/sass/partials/_sidebar--left.scss b/web/sass-files/sass/partials/_sidebar--left.scss
index 6a418e270..73d702fef 100644
--- a/web/sass-files/sass/partials/_sidebar--left.scss
+++ b/web/sass-files/sass/partials/_sidebar--left.scss
@@ -16,12 +16,6 @@
overflow-y: auto;
max-width: 200px;
width: 200px;
- a {
- color: #262626 !important;
- &:hover, &:focus {
- background: #f5f5f5 !important;
- }
- }
}
.search__form {
margin: 0;
@@ -75,7 +69,7 @@
top: 66px;
}
.nav-pills__unread-indicator-bottom {
- bottom: 0px;
+ bottom: 10px;
}
.nav {
@@ -98,7 +92,6 @@
padding: 3px 10px 3px 25px;
line-height: 1.5;
border-radius: 0;
- color: #999;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -109,12 +102,8 @@
text-decoration: underline;
}
&.unread-title {
- color: #333;
font-weight: 600;
}
- &:hover, &:focus {
- background: #e6f2fa;
- }
}
&.active {
a, a:hover, a:focus {
diff --git a/web/sass-files/sass/partials/_sidebar--menu.scss b/web/sass-files/sass/partials/_sidebar--menu.scss
index 4366b1a6c..6f4a0cc38 100644
--- a/web/sass-files/sass/partials/_sidebar--menu.scss
+++ b/web/sass-files/sass/partials/_sidebar--menu.scss
@@ -54,7 +54,7 @@
> a {
font-size: 15px;
background: none !important;
- color: #444;
+ color: inherit;
line-height: 40px;
padding: 0 15px;
.glyphicon {
diff --git a/web/sass-files/sass/styles.scss b/web/sass-files/sass/styles.scss
index e704536a8..c614052da 100644
--- a/web/sass-files/sass/styles.scss
+++ b/web/sass-files/sass/styles.scss
@@ -41,3 +41,6 @@
// Responsive Css
@import "partials/responsive";
+
+// Standalone Css
+@import "partials/oauth";
diff --git a/web/static/images/themes/mattermost dark.png b/web/static/images/themes/mattermost dark.png
index 832f64d2e..1ab44e104 100644
--- a/web/static/images/themes/mattermost dark.png
+++ b/web/static/images/themes/mattermost dark.png
Binary files differ
diff --git a/web/static/images/themes/mattermost.png b/web/static/images/themes/mattermost.png
index 4a321adcb..8dea37ff6 100644
--- a/web/static/images/themes/mattermost.png
+++ b/web/static/images/themes/mattermost.png
Binary files differ
diff --git a/web/static/images/themes/organization.png b/web/static/images/themes/organization.png
index 1a38bfb34..3066cca0b 100644
--- a/web/static/images/themes/organization.png
+++ b/web/static/images/themes/organization.png
Binary files differ
diff --git a/web/static/images/themes/slack.png b/web/static/images/themes/slack.png
deleted file mode 100644
index dc70c7dc2..000000000
--- a/web/static/images/themes/slack.png
+++ /dev/null
Binary files differ
diff --git a/web/static/images/themes/windows dark.png b/web/static/images/themes/windows dark.png
index d65304820..1f665d882 100644
--- a/web/static/images/themes/windows dark.png
+++ b/web/static/images/themes/windows dark.png
Binary files differ
diff --git a/web/templates/authorize.html b/web/templates/authorize.html
index 3392c1b1e..b0fa3e475 100644
--- a/web/templates/authorize.html
+++ b/web/templates/authorize.html
@@ -1,26 +1,26 @@
{{define "authorize"}}
<html>
{{template "head" . }}
-<body class="white">
- <div class="container-fluid">
- <div class="inner__wrap">
- <div class="row content">
- <div class="signup-header">
- {{.Props.TeamName}}
- </div>
- <div class="col-sm-12">
- <div id="authorize"></div>
- </div>
- <div class="footer-push"></div>
- </div>
- <div class="row footer">
- {{template "footer" . }}
- </div>
- </div>
- </div>
- <script>
- window.setup_authorize_page('{{ .Props.TeamName }}', '{{ .Props.AppName }}', '{{ .Props.ResponseType }}', '{{ .Props.ClientId }}', '{{ .Props.RedirectUri }}', '{{ .Props.Scope }}', '{{ .Props.State }}' );
- </script>
+<body>
+ <div class="container-fluid">
+ <div class="oauth-prompt">
+ <div class="prompt__heading">
+ <div class="prompt__app-icon">
+ <img src="/static/images/icon50x50.gif" width="50" height="50" alt="">
+ </div>
+ <div class="text">An application would like to connect to your {{.Props.TeamName}} account.</div>
+ </div>
+ <p>The app <strong>{{.Props.AppName}}</strong> would like the ability to access Mattermost on your behalf.</p>
+ <h2 class="prompt__allow">Allow <strong>{{.Props.AppName}}</strong> access?</h2>
+ <div class="prompt__buttons">
+ <input type="button" class="btn btn-link" value="Deny">
+ <input type="button" class="btn btn-primary" value="Allow">
+ </div>
+ </div>
+ </div>
+ <script>
+ window.setup_authorize_page('{{.Props}}');
+ </script>
</body>
</html>
{{end}}
diff --git a/web/web.go b/web/web.go
index bf985a5a0..a1bbf5a81 100644
--- a/web/web.go
+++ b/web/web.go
@@ -189,9 +189,40 @@ func login(c *api.Context, w http.ResponseWriter, r *http.Request) {
return
}
+ // We still might be able to switch to this team because we've logged in before
+ if multiCookie, err := r.Cookie(model.MULTI_SESSION_TOKEN); err == nil {
+ multiToken := multiCookie.Value
+
+ if len(multiToken) > 0 {
+ tokens := strings.Split(multiToken, " ")
+
+ for _, token := range tokens {
+ if sr := <-api.Srv.Store.Session().Get(token); sr.Err == nil {
+ s := sr.Data.(*model.Session)
+
+ if !s.IsExpired() && s.TeamId == team.Id {
+ w.Header().Set(model.HEADER_TOKEN, s.Token)
+ sessionCookie := &http.Cookie{
+ Name: model.SESSION_TOKEN,
+ Value: s.Token,
+ Path: "/",
+ MaxAge: model.SESSION_TIME_WEB_IN_SECS,
+ HttpOnly: true,
+ }
+
+ http.SetCookie(w, sessionCookie)
+
+ http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/channels/town-square", http.StatusTemporaryRedirect)
+ return
+ }
+ }
+ }
+ }
+ }
+
page := NewHtmlTemplatePage("login", "Login")
page.Props["TeamDisplayName"] = team.DisplayName
- page.Props["TeamName"] = teamName
+ page.Props["TeamName"] = team.Name
page.Render(c, w)
}
@@ -288,6 +319,10 @@ func logout(c *api.Context, w http.ResponseWriter, r *http.Request) {
func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
name := params["channelname"]
+ teamName := params["team"]
+
+ var team *model.Team
+ teamChan := api.Srv.Store.Team().Get(c.Session.TeamId)
var channelId string
if result := <-api.Srv.Store.Channel().CheckPermissionsToByName(c.Session.TeamId, name, c.Session.UserId); result.Err != nil {
@@ -297,6 +332,19 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
channelId = result.Data.(string)
}
+ if tResult := <-teamChan; tResult.Err != nil {
+ c.Err = tResult.Err
+ return
+ } else {
+ team = tResult.Data.(*model.Team)
+ }
+
+ if team.Name != teamName {
+ l4g.Error("It appears you are logged into " + team.Name + ", but are trying to access " + teamName)
+ http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/channels/town-square", http.StatusFound)
+ return
+ }
+
if len(channelId) == 0 {
if strings.Index(name, "__") > 0 {
// It's a direct message channel that doesn't exist yet so let's create it
@@ -319,7 +367,7 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
// lets make sure the user is valid
if result := <-api.Srv.Store.User().Get(c.Session.UserId); result.Err != nil {
c.Err = result.Err
- c.RemoveSessionCookie(w)
+ c.RemoveSessionCookie(w, r)
l4g.Error("Error in getting users profile for id=%v forcing logout", c.Session.UserId)
return
}
@@ -332,18 +380,10 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
}
}
- var team *model.Team
-
- if tResult := <-api.Srv.Store.Team().Get(c.Session.TeamId); tResult.Err != nil {
- c.Err = tResult.Err
- return
- } else {
- team = tResult.Data.(*model.Team)
- }
-
page := NewHtmlTemplatePage("channel", "")
page.Props["Title"] = name + " - " + team.DisplayName + " " + page.ClientProps["SiteName"]
page.Props["TeamDisplayName"] = team.DisplayName
+ page.Props["TeamName"] = team.Name
page.Props["TeamType"] = team.Type
page.Props["TeamId"] = team.Id
page.Props["ChannelName"] = name
@@ -451,6 +491,7 @@ func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) {
page := NewHtmlTemplatePage("password_reset", "")
page.Props["Title"] = "Reset Password " + page.ClientProps["SiteName"]
page.Props["TeamDisplayName"] = teamDisplayName
+ page.Props["TeamName"] = teamName
page.Props["Hash"] = hash
page.Props["Data"] = data
page.Props["TeamName"] = teamName
@@ -843,6 +884,12 @@ func getAccessToken(c *api.Context, w http.ResponseWriter, r *http.Request) {
}
func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
+ if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
+ c.Err = model.NewAppError("incomingWebhook", "Incoming webhooks have been disabled by the system admin.", "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
+
params := mux.Vars(r)
id := params["id"]
@@ -865,6 +912,9 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
channelName := props["channel"]
+ overrideUsername := props["username"]
+ overrideIconUrl := props["icon_url"]
+
var hook *model.IncomingWebhook
if result := <-hchan; result.Err != nil {
c.Err = model.NewAppError("incomingWebhook", "Invalid webhook", "err="+result.Err.Message)
@@ -910,6 +960,15 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
pchan := api.Srv.Store.Channel().CheckPermissionsTo(hook.TeamId, channel.Id, hook.UserId)
post := &model.Post{UserId: hook.UserId, ChannelId: channel.Id, Message: text}
+ post.AddProp("from_webhook", "true")
+
+ if len(overrideUsername) != 0 && utils.Cfg.ServiceSettings.EnablePostUsernameOverride {
+ post.AddProp("override_username", overrideUsername)
+ }
+
+ if len(overrideIconUrl) != 0 && utils.Cfg.ServiceSettings.EnablePostIconOverride {
+ post.AddProp("override_icon_url", overrideIconUrl)
+ }
if !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN {
c.Err = model.NewAppError("incomingWebhook", "Inappropriate channel permissions", "")