summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md235
-rw-r--r--api/command.go112
-rw-r--r--api/command_test.go37
-rw-r--r--api/templates/error.html2
-rw-r--r--api/templates/find_teams_body.html2
-rw-r--r--api/templates/signup_team_body.html2
-rw-r--r--doc/README.md5
-rw-r--r--doc/config/smtp-email-setup.md38
-rw-r--r--doc/install/aws-ebs-setup.md27
-rw-r--r--doc/install/requirements.md (renamed from requirements.md)0
-rw-r--r--doc/install/single-container-install.md120
-rw-r--r--model/command.go2
-rw-r--r--store/sql_post_store.go14
-rw-r--r--store/sql_post_store_test.go17
-rw-r--r--web/react/.eslintrc4
-rw-r--r--web/react/components/command_list.jsx6
-rw-r--r--web/react/components/post_info.jsx17
-rw-r--r--web/react/components/post_list.jsx8
-rw-r--r--web/react/components/setting_item_max.jsx2
-rw-r--r--web/react/components/sidebar_header.jsx2
-rw-r--r--web/react/components/user_settings_general.jsx237
-rw-r--r--web/react/components/user_settings_notifications.jsx16
-rw-r--r--web/react/stores/channel_store.jsx1
-rw-r--r--web/react/utils/utils.jsx58
-rw-r--r--web/sass-files/sass/partials/_command-box.scss16
-rw-r--r--web/sass-files/sass/partials/_settings.scss5
-rw-r--r--web/templates/head.html2
27 files changed, 620 insertions, 367 deletions
diff --git a/README.md b/README.md
index 1dd9620d6..b469754ea 100644
--- a/README.md
+++ b/README.md
@@ -1,214 +1,55 @@
-**Mattermost Alpha**
-**Team Communication Service**
-**Development Build**
+# Mattermost is an open source, on-prem Slack Alternative
+Mattermost modernizes team communication without locking in your data to a single provider.
-About Mattermost
-================
+Offer your end users messaging and file sharing across PCs and phones with archiving and instant search--without losing control of your data.
-Mattermost is an open-source team communication service. It brings team messaging and file sharing into one place, accessible across PCs and phones, with archiving and search.
+## Features
-Learn More
-==========
-- Ask the core team anything at: http://forum.mattermost.org
-- Share feature requests and upvotes: http://www.mattermost.org/feature-requests/
-- File bugs: http://www.mattermost.org/filing-issues/
-- Make a pull request: http://www.mattermost.org/contribute-to-mattermost/
+### Sharing Messaging and Files
+- Send messages and comments across public, private and 1-1 channels
+- Personalize notifications for unreads and mentions by channel and keyword
+- Share files and images internally and externally
-Installing Mattermost
-=====================
+### Archiving and Search
-You're installing "Mattermost Alpha", a pre-released version providing an early look at what we're building. While the core team runs this version internally, it's not recommended for production since we can't guarantee API stability or backwards compatibility.
+- Search archives across channels for historical messages and comments
+- Use #hashtags to tag and recall messages, threads and files
+- View recent mentions of your name and custom search terms
-That said, any issues at all, please let us know on the Mattermost forum at: http://forum.mattermost.org
+### Anywhere Access
-Notes:
-- For Alpha, Docker is intentionally setup as a single container, since production deployment is not yet recommended.
+- Use Mattermost from web-enabled PCs and phones
+- Attach sound, video and image files from mobile devices
+- Define team-specific color themes across your devices
-Local Machine Setup (Docker)
------------------------------
+## Learn More
-### Mac OSX ###
+- [Product Vision and Target Audiences](http://www.mattermost.org/vision/) - What we're solving and for whom are we building
+- [Mattermost Forum](http://forum.mattermost.org/) - For technical questions and answers
+- [Issue Tracker](http://www.mattermost.org/filing-issues/) - For reporting bugs
+- [Feature Ideas Forum](http://www.mattermost.org/feature-requests/) - For sharing ideas for future versions
+- [Contributuion Guidelines](http://www.mattermost.org/contribute-to-mattermost/) - For contributing code or feedback to the project
-1. Install Boot2Docker using instructions at: http://docs.docker.com/installation/mac/
- 1. Start Boot2Docker from the command line and run: `boot2docker init eval “$(boot2docker shellinit)”`
-2. Get your Docker IP address with: `boot2docker ip`
-3. Use `sudo nano /etc/hosts` to add `<Docker IP> dockerhost` to your /etc/hosts file
-4. Run: `boot2docker shellinit` and copy the export statements to your ~/.bash\_profile by running `sudo nano ~/.bash_profile`. Then run: `source ~/.bash_profile`
-5. Run: `docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform`
-6. When docker is done fetching the image, open http://dockerhost:8065/ in your browser.
+Follow us on Twitter at [@MattermostHQ](https://twitter.com/mattermosthq).
-### Ubuntu ###
-1. Follow the instructions at https://docs.docker.com/installation/ubuntulinux/ or use the summary below:
+# Installing Mattermost
- ``` bash
- sudo apt-get update
- sudo apt-get install wget
- wget -qO- https://get.docker.com/ | sh
- sudo usermod -aG docker <username>
- sudo service docker start
- newgrp docker
- ```
+Depending on your needs, there are multiple ways to install Mattermost:
-2. Start docker container:
+### Product Evaluation
- ``` bash
- docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform
- ```
-
-3. When docker is done fetching the image, open http://localhost:8065/ in your browser.
-
-### Arch ###
-1. Install Docker using the following commands:
-
- ``` bash
- pacman -S docker
- systemctl enable docker.service
- systemctl start docker.service
- gpasswd -a <username> docker
- newgrp docker
- ```
-
-2. Start Docker container:
-
- ``` bash
- docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform
- ```
-
-3. When Docker is done fetching the image, open http://localhost:8065/ in your browser.
-
-### Additional Notes ###
-- If you want to work with the latest master from the repository (i.e. not a stable release) you can run the cmd:
-
- ``` bash
- docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:dev
- ```
-
-- Instructions on how to update your Docker image are found below.
-
-- If you wish to remove mattermost-dev use:
-
- ``` bash
- docker stop mattermost-dev
- docker rm -v mattermost-dev
- ```
-
-- If you wish to gain access to a shell on the container use:
-
- ``` bash
- docker exec -ti mattermost-dev /bin/bash
- ```
-
-AWS Elastic Beanstalk Setup (Docker)
-------------------------------------
-
-1. Create a new Elastic Beanstalk Docker application using the [Dockerrun.aws.zip](docker/0.6/Dockerrun.aws//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.6/Dockerrun.aws//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.
- 11. You can set the configuration details as you please but they may be left at their defaults. When you are done press next.
- 12. Environment tags my be left blank. Press next.
- 13. You will be asked to review your information. Press Launch.
-
-4. Try it out!
- 14. Wait for beanstalk to update the environment.
- 15. Try it out by entering the domain of the form \*.elasticbeanstalk.com found at the top of the dashboard into your browser. You can also map your own domain if you wish.
-
-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.
-
-* *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.
-* *ServiceSettings*:*StorageDirectory* - The file path where files will be stored locally if *UseLocalStorage* is set to true. The operating system user that is running the Mattermost application must have read and write privileges to this directory.
-* *AWSSettings*:*S3*\* - If *UseLocalStorage* is set to false, and the S3 settings are configured here, then Mattermost will store files in the provided S3 bucket.
-
-Email Setup (Optional)
-----------------------
-
-1. Setup an email sending service. If you already have credentials for a SMTP server you can skip this step.
- 1. [Setup Amazon Simple Email Service](https://console.aws.amazon.com/ses)
- 2. From the `SMTP Settings` menu click `Create My SMTP Credentials`
- 3. Copy the `Server Name`, `Port`, `SMTP Username`, and `SMTP Password`
- 4. From the `Domains` menu setup and verify a new domain. It it also a good practice to enable `Generate DKIM Settings` for this domain.
- 5. Choose an email address like `feedback@example.com` for Mattermost to send emails from.
- 6. Test sending an email from `feedback@example.com` by clicking the `Send a Test Email` button and verify everything appears to be working correctly.
-2. Modify the Mattermost configuration file config.json or config_docker.json with the SMTP information.
- 1. If you're running Mattermost on Amazon Beanstalk you can shell into the instance with the following commands
- 2. `ssh ec2-user@[domain for the docker instance]`
- 3. `sudo gpasswd -a ec2-user docker`
- 4. Retrieve the name of the container with `sudo docker ps`
- 5. `sudo docker exec -ti container_name /bin/bash`
-2. Edit the config file `vi /config_docker.json` with the settings you captured from the step above. See an example below and notice `ByPassEmail` has been set to `false`
-
-``` bash
-"EmailSettings": {
- "ByPassEmail" : false,
- "SMTPUsername": "AKIADTOVBGERKLCBV",
- "SMTPPassword": "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY",
- "SMTPServer": "email-smtp.us-east-1.amazonaws.com:465",
- "UseTLS": true,
- "FeedbackEmail": "feedback@example.com",
- "FeedbackName": "Feedback",
- "ApplePushServer": "",
- "ApplePushCertPublic": "",
- "ApplePushCertPrivate": ""
-}
-```
-
-3. Restart Mattermost
- 1. Find the process id with `ps -A` and look for the process named `platform`
- 2. Kill the process `kill pid`
- 3. The service should restart automatically. Verify the Mattermost service is running with `ps -A`
- 4. Current logged in users will not be affected, but upon logging out or session expiration users will be required to verify their email address.
-
-Upgrading Mattermost Preview
-----------------------------
-
-### Docker ###
-To upgrade your Docker image to a preview of the latest stable release (NOTE: this will erase all data in the Docker container, including the database):
-
-1. Stop your Docker container by running:
-
- ``` bash
- docker stop mattermost-dev
- ```
-2. Delete your Docker container by running:
-
- ``` bash
- docker rm mattermost-dev
- ```
-3. Update your Docker image by running:
-
- ``` bash
- docker pull mattermost/platform
- ```
-4. Start your Docker container by running:
-
- ``` bash
- docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform
- ```
-
-To upgrade to the latest development build on master from the repository replace `mattermost/platform` with `mattermost/platform:dev` in the instructions 3) and 4) above.
-
-Contributing
-------------
-
-To contribute to this open source project please review the [Mattermost Contribution Guidelines]( http://www.mattermost.org/contribute-to-mattermost/).
-
-To setup your machine for development of mattermost see: [Developer Machine Setup](scripts/README_DEV.md).
-
-License
--------
-
-Mattermost is licensed under an "Apache-wrapped AGPL" model inspired by MongoDB. Similar to MongoDB, you can run and link to the system using Configuration Files and Admin Tools licensed under Apache, version 2.0, as described in the LICENSE file, as an explicit exception to the terms of the GNU Affero General Public License (AGPL) that applies to most of the remaining source files. See individual files for details.
+- [Local Machine Install and Upgrade 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.
+- [AWS EBS Install and Upgrade 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.
+
+### Development
+
+- [Developer Machine Setup](scripts/README_DEV.md) - Setup your local machine development environment using Docker on Mac OSX or Ubuntu.
+
+### Production Deployment
+
+- [GitLab Mattermost Production Installation](https://about.gitlab.com/downloads/) - Install Mattermost for production environments bundled with GitLab, a leading open source Git repository, using an omnibus package for Ubuntu 12.04, Ubuntu 14.04, Debian 7, Debian 8, and CentOS 6 (and RedHat/Oracle/Scientific Linux 6), CentOS 7 (and RedHat/Oracle/Scientific Linux 7).
+
+Any issues at all, please let us know on the Mattermost forum at: http://forum.mattermost.org
diff --git a/api/command.go b/api/command.go
index f051bd42e..749cbf790 100644
--- a/api/command.go
+++ b/api/command.go
@@ -4,15 +4,15 @@
package api
import (
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
l4g "code.google.com/p/log4go"
"github.com/gorilla/mux"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
- "net/http"
- "reflect"
- "runtime"
- "strconv"
- "strings"
)
type commandHandler func(c *Context, command *model.Command) bool
@@ -24,6 +24,8 @@ var commands = []commandHandler{
echoCommand,
}
+var echoSem chan bool
+
func InitCommand(r *mux.Router) {
l4g.Debug("Initializing command api routes")
r.Handle("/command", ApiUserRequired(command)).Methods("POST")
@@ -41,7 +43,6 @@ func command(c *Context, w http.ResponseWriter, r *http.Request) {
}
checkCommand(c, command)
-
if c.Err != nil {
return
} else {
@@ -56,8 +57,6 @@ func checkCommand(c *Context, command *model.Command) bool {
return false
}
- tchan := Srv.Store.Team().Get(c.Session.TeamId)
-
if len(command.ChannelId) > 0 {
cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, command.ChannelId, c.Session.UserId)
@@ -66,24 +65,9 @@ func checkCommand(c *Context, command *model.Command) bool {
}
}
- allowValet := false
- if tResult := <-tchan; tResult.Err != nil {
- c.Err = model.NewAppError("checkCommand", "Could not find the team for this session, team_id="+c.Session.TeamId, "")
- return false
- } else {
- allowValet = tResult.Data.(*model.Team).AllowValet
- }
-
- ec := runtime.FuncForPC(reflect.ValueOf(echoCommand).Pointer()).Name()
-
for _, v := range commands {
- if !allowValet && ec == runtime.FuncForPC(reflect.ValueOf(v).Pointer()).Name() {
- continue
- }
- if v(c, command) {
- return true
- } else if c.Err != nil {
+ if v(c, command) || c.Err != nil {
return true
}
}
@@ -112,55 +96,65 @@ func logoutCommand(c *Context, command *model.Command) bool {
}
func echoCommand(c *Context, command *model.Command) bool {
-
cmd := "/echo"
+ maxThreads := 100
- if strings.Index(command.Command, cmd) == 0 {
- parts := strings.SplitN(command.Command, " ", 3)
-
- channelName := ""
- if len(parts) >= 2 {
- channelName = parts[1]
+ if !command.Suggest && strings.Index(command.Command, cmd) == 0 {
+ parameters := strings.SplitN(command.Command, " ", 2)
+ if len(parameters) != 2 || len(parameters[1]) == 0 {
+ return false
}
-
- message := ""
- if len(parts) >= 3 {
- message = parts[2]
+ message := strings.Trim(parameters[1], " ")
+ delay := 0
+ if endMsg := strings.LastIndex(message, "\""); string(message[0]) == "\"" && endMsg > 1 {
+ if checkDelay, err := strconv.Atoi(strings.Trim(message[endMsg:], " \"")); err == nil {
+ delay = checkDelay
+ }
+ message = message[1:endMsg]
+ } else if strings.Index(message, " ") > -1 {
+ delayIdx := strings.LastIndex(message, " ")
+ delayStr := strings.Trim(message[delayIdx:], " ")
+
+ if checkDelay, err := strconv.Atoi(delayStr); err == nil {
+ delay = checkDelay
+ message = message[:delayIdx]
+ }
}
- if result := <-Srv.Store.Channel().GetChannels(c.Session.TeamId, c.Session.UserId); result.Err != nil {
- c.Err = result.Err
+ if delay > 10000 {
+ c.Err = model.NewAppError("echoCommand", "Delays must be under 10000 seconds", "")
return false
- } else {
- channels := result.Data.(*model.ChannelList)
+ }
- for _, v := range channels.Channels {
- if v.Type == model.CHANNEL_DIRECT {
- continue
- }
+ if echoSem == nil {
+ // We want one additional thread allowed so we never reach channel lockup
+ echoSem = make(chan bool, maxThreads+1)
+ }
- if v.Name == channelName && !command.Suggest {
- post := &model.Post{}
- post.ChannelId = v.Id
- post.Message = message
+ if len(echoSem) >= maxThreads {
+ c.Err = model.NewAppError("echoCommand", "High volume of echo request, cannot process request", "")
+ return false
+ }
- if _, err := CreateValetPost(c, post); err != nil {
- c.Err = err
- return false
- }
+ echoSem <- true
+ go func() {
+ defer func() { <-echoSem }()
+ post := &model.Post{}
+ post.ChannelId = command.ChannelId
+ post.Message = message
- command.Response = model.RESP_EXECUTED
- return true
- }
+ time.Sleep(time.Duration(delay) * time.Second)
- if len(channelName) == 0 || (strings.Index(v.Name, channelName) == 0 && len(parts) < 3) {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd + " " + v.Name, Description: "Echo a message using Valet in a channel"})
- }
+ if _, err := CreatePost(c, post, false); err != nil {
+ l4g.Error("Unable to create /echo post, err=%v", err)
}
- }
+ }()
+
+ command.Response = model.RESP_EXECUTED
+ return true
} else if strings.Index(cmd, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: "Echo a message using Valet in a channel"})
+ command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: "Echo back text from your account, /echo \"message\" [delay in seoncds]"})
}
return false
diff --git a/api/command_test.go b/api/command_test.go
index a58ef9be5..fe52dd41b 100644
--- a/api/command_test.go
+++ b/api/command_test.go
@@ -4,9 +4,10 @@
package api
import (
+ "testing"
+
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
- "testing"
)
func TestSuggestRootCommands(t *testing.T) {
@@ -50,6 +51,12 @@ func TestSuggestRootCommands(t *testing.T) {
if rs3.Suggestions[0].Suggestion != "/join" {
t.Fatal("should have join cmd")
}
+
+ rs4 := Client.Must(Client.Command("", "/ech", true)).Data.(*model.Command)
+
+ if rs4.Suggestions[0].Suggestion != "/echo" {
+ t.Fatal("should have echo cmd")
+ }
}
func TestLogoutCommands(t *testing.T) {
@@ -145,3 +152,31 @@ func TestJoinCommands(t *testing.T) {
t.Fatal("didn't join channel")
}
}
+
+func TestEchoCommand(t *testing.T) {
+ Setup()
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user1.Id))
+
+ Client.LoginByEmail(team.Name, user1.Email, "pwd")
+
+ channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
+
+ echoTestString := "/echo test"
+
+ r1 := Client.Must(Client.Command(channel1.Id, echoTestString, false)).Data.(*model.Command)
+ if r1.Response != model.RESP_EXECUTED {
+ t.Fatal("Echo command failed to execute")
+ }
+
+ p1 := Client.Must(Client.GetPosts(channel1.Id, 0, 2, "")).Data.(*model.PostList)
+ if len(p1.Order) != 1 {
+ t.Fatal("Echo command failed to send")
+ }
+}
diff --git a/api/templates/error.html b/api/templates/error.html
index 3474c9e1e..adb8f9f7d 100644
--- a/api/templates/error.html
+++ b/api/templates/error.html
@@ -5,7 +5,7 @@
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script>
- <link href='http://fonts.googleapis.com/css?family=Open+Sans:400,600,700' rel='stylesheet' type='text/css'>
+ <link href='https://fonts.googleapis.com/css?family=Open+Sans:400,600,700' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="/static/css/styles.css">
</head>
<body class="white error">
diff --git a/api/templates/find_teams_body.html b/api/templates/find_teams_body.html
index 64bff8126..00c5628dd 100644
--- a/api/templates/find_teams_body.html
+++ b/api/templates/find_teams_body.html
@@ -19,7 +19,7 @@
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
<h2 style="font-weight: normal; margin-top: 10px;">Finding teams</h2>
<p>{{ if .Props }}
- The following teams were found:<br>
+ Your request to find teams associated with your email found the following:<br>
{{range $index, $element := .Props}}
<a href="{{ $element }}" style="text-decoration: none; color:#2389D7;">{{ $index }}</a><br>
{{ end }}
diff --git a/api/templates/signup_team_body.html b/api/templates/signup_team_body.html
index 71df0b9c8..b49cf5f36 100644
--- a/api/templates/signup_team_body.html
+++ b/api/templates/signup_team_body.html
@@ -21,7 +21,7 @@
<p style="margin: 20px 0 25px">
<a href="{{.Props.Link}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Set up your team</a>
</p>
- {{ .SiteName }} is free for an unlimited time, for unlimited users.<br>You'll get more out of {{ .SiteName }} when your team is in constant communication--let's get them on board.<br></p>
+ {{ .SiteName }} is one place for all your team communication, searchable and available anywhere.<br>You'll get more out of {{ .SiteName }} when your team is in constant communication--let's get them on board.<br></p>
<p>
Learn more by <a href="{{.Props.TourUrl}}" style="text-decoration: none; color:#2389D7;">taking a tour</a>
</p>
diff --git a/doc/README.md b/doc/README.md
new file mode 100644
index 000000000..5195327b2
--- /dev/null
+++ b/doc/README.md
@@ -0,0 +1,5 @@
+# Documentation
+
+## Administrator Documentation
+
+- [GitLab SSO Configuration](integrations/sso/gitlab-sso.md) - Configure OAuth2 Single-Sign-On for GitLab.
diff --git a/doc/config/smtp-email-setup.md b/doc/config/smtp-email-setup.md
new file mode 100644
index 000000000..b90d78919
--- /dev/null
+++ b/doc/config/smtp-email-setup.md
@@ -0,0 +1,38 @@
+
+## SMTP Email Setup
+
+The following instructions maybe used when SMTP email is not setup as part of the installation process.
+
+1. Setup an email sending service. If you already have credentials for a SMTP server you can skip this step.
+ 1. [Setup Amazon Simple Email Service](https://console.aws.amazon.com/ses)
+ 2. From the `SMTP Settings` menu click `Create My SMTP Credentials`
+ 3. Copy the `Server Name`, `Port`, `SMTP Username`, and `SMTP Password`
+ 4. From the `Domains` menu setup and verify a new domain. It it also a good practice to enable `Generate DKIM Settings` for this domain.
+ 5. Choose an email address like `feedback@example.com` for Mattermost to send emails from.
+ 6. Test sending an email from `feedback@example.com` by clicking the `Send a Test Email` button and verify everything appears to be working correctly.
+2. Modify the Mattermost configuration file config.json or config_docker.json with the SMTP information.
+ 1. If you're running Mattermost on Amazon Beanstalk you can shell into the instance with the following commands
+ 2. `ssh ec2-user@[domain for the docker instance]`
+ 3. `sudo gpasswd -a ec2-user docker`
+ 4. Retrieve the name of the container with `sudo docker ps`
+ 5. `sudo docker exec -ti container_name /bin/bash`
+3. Edit the config file `vi /config_docker.json` with the settings you captured from the step above. See an example below and notice `ByPassEmail` has been set to `false`
+``` bash
+"EmailSettings": {
+ "ByPassEmail" : false,
+ "SMTPUsername": "AKIADTOVBGERKLCBV",
+ "SMTPPassword": "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY",
+ "SMTPServer": "email-smtp.us-east-1.amazonaws.com:465",
+ "UseTLS": true,
+ "FeedbackEmail": "feedback@example.com",
+ "FeedbackName": "Feedback",
+ "ApplePushServer": "",
+ "ApplePushCertPublic": "",
+ "ApplePushCertPrivate": ""
+}
+```
+4. Restart Mattermost
+ 1. Find the process id with `ps -A` and look for the process named `platform`
+ 2. Kill the process `kill pid`
+ 3. The service should restart automatically. Verify the Mattermost service is running with `ps -A`
+ 4. Current logged in users will not be affected, but upon logging out or session expiration users will be required to verify their email address.
diff --git a/doc/install/aws-ebs-setup.md b/doc/install/aws-ebs-setup.md
new file mode 100644
index 000000000..452cfcb4f
--- /dev/null
+++ b/doc/install/aws-ebs-setup.md
@@ -0,0 +1,27 @@
+
+## AWS Elastic Beanstalk Setup (Docker)
+
+1. Create a new Elastic Beanstalk Docker application using the [Dockerrun.aws.zip](docker/0.6/Dockerrun.aws//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.6/Dockerrun.aws//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.
+ 11. You can set the configuration details as you please but they may be left at their defaults. When you are done press next.
+ 12. Environment tags my be left blank. Press next.
+ 13. You will be asked to review your information. Press Launch.
+
+4. Try it out!
+ 14. Wait for beanstalk to update the environment.
+ 15. Try it out by entering the domain of the form \*.elasticbeanstalk.com found at the top of the dashboard into your browser. You can also map your own domain if you wish.
+
+
+ ### (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).
diff --git a/requirements.md b/doc/install/requirements.md
index cc0d1833d..cc0d1833d 100644
--- a/requirements.md
+++ b/doc/install/requirements.md
diff --git a/doc/install/single-container-install.md b/doc/install/single-container-install.md
new file mode 100644
index 000000000..3e307ca74
--- /dev/null
+++ b/doc/install/single-container-install.md
@@ -0,0 +1,120 @@
+# Single Container Installation and Upgrade
+
+The following install instructions are for single-container installs of Mattermost using Docker for exploring product functionality and upgrading to newer versions.
+
+Local Machine Setup (Docker)
+-----------------------------
+
+### Mac OSX ###
+
+1. Install Boot2Docker using instructions at: http://docs.docker.com/installation/mac/
+ 1. Start Boot2Docker from the command line and run: `boot2docker init eval “$(boot2docker shellinit)”`
+2. Get your Docker IP address with: `boot2docker ip`
+3. Use `sudo nano /etc/hosts` to add `<Docker IP> dockerhost` to your /etc/hosts file
+4. Run: `boot2docker shellinit` and copy the export statements to your ~/.bash\_profile by running `sudo nano ~/.bash_profile`. Then run: `source ~/.bash_profile`
+5. Run: `docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform`
+6. When docker is done fetching the image, open http://dockerhost:8065/ in your browser.
+
+### Ubuntu ###
+1. Follow the instructions at https://docs.docker.com/installation/ubuntulinux/ or use the summary below:
+
+ ``` bash
+ sudo apt-get update
+ sudo apt-get install wget
+ wget -qO- https://get.docker.com/ | sh
+ sudo usermod -aG docker <username>
+ sudo service docker start
+ newgrp docker
+ ```
+
+2. Start docker container:
+
+ ``` bash
+ docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform
+ ```
+
+3. When docker is done fetching the image, open http://localhost:8065/ in your browser.
+
+### Arch ###
+1. Install Docker using the following commands:
+
+ ``` bash
+ pacman -S docker
+ systemctl enable docker.service
+ systemctl start docker.service
+ gpasswd -a <username> docker
+ newgrp docker
+ ```
+
+2. Start Docker container:
+
+ ``` bash
+ docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform
+ ```
+
+3. When Docker is done fetching the image, open http://localhost:8065/ in your browser.
+
+### Additional Notes ###
+- If you want to work with the latest master from the repository (i.e. not a stable release) you can run the cmd:
+
+ ``` bash
+ docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:dev
+ ```
+
+- Instructions on how to update your Docker image are found below.
+
+- If you wish to remove mattermost-dev use:
+
+ ``` bash
+ docker stop mattermost-dev
+ docker rm -v mattermost-dev
+ ```
+
+- If you wish to gain access to a shell on the container use:
+
+ ``` bash
+ docker exec -ti mattermost-dev /bin/bash
+ ```
+
+## 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.
+
+* *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.
+* *ServiceSettings*:*StorageDirectory* - The file path where files will be stored locally if *UseLocalStorage* is set to true. The operating system user that is running the Mattermost application must have read and write privileges to this directory.
+* *AWSSettings*:*S3*\* - If *UseLocalStorage* is set to false, and the S3 settings are configured here, then Mattermost will store files in the provided S3 bucket.
+
+### (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).
+
+## Upgrading Mattermost
+
+### Docker ###
+To upgrade your Docker image to a preview of the latest stable release (NOTE: this will erase all data in the Docker container, including the database):
+
+1. Stop your Docker container by running:
+
+ ``` bash
+ docker stop mattermost-dev
+ ```
+2. Delete your Docker container by running:
+
+ ``` bash
+ docker rm mattermost-dev
+ ```
+3. Update your Docker image by running:
+
+ ``` bash
+ docker pull mattermost/platform
+ ```
+4. Start your Docker container by running:
+
+ ``` bash
+ docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform
+ ```
+
+To upgrade to the latest development build on master from the repository replace `mattermost/platform` with `mattermost/platform:dev` in the instructions 3) and 4) above.
diff --git a/model/command.go b/model/command.go
index 23573205e..83243cc98 100644
--- a/model/command.go
+++ b/model/command.go
@@ -14,7 +14,7 @@ const (
type Command struct {
Command string `json:"command"`
- Response string `json:"reponse"`
+ Response string `json:"response"`
GotoLocation string `json:"goto_location"`
ChannelId string `json:"channel_id"`
Suggest bool `json:"-"`
diff --git a/store/sql_post_store.go b/store/sql_post_store.go
index cd668b13c..297d60397 100644
--- a/store/sql_post_store.go
+++ b/store/sql_post_store.go
@@ -5,9 +5,11 @@ package store
import (
"fmt"
+ "regexp"
+ "strings"
+
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
- "strings"
)
type SqlPostStore struct {
@@ -358,7 +360,7 @@ func (s SqlPostStore) getParentsPosts(channelId string, offset int, limit int) S
var posts []*model.Post
_, err := s.GetReplica().Select(&posts,
- `SELECT
+ `SELECT
q2.*
FROM
Posts q2
@@ -366,7 +368,7 @@ func (s SqlPostStore) getParentsPosts(channelId string, offset int, limit int) S
(SELECT DISTINCT
q3.RootId
FROM
- (SELECT
+ (SELECT
RootId
FROM
Posts
@@ -417,6 +419,12 @@ func (s SqlPostStore) Search(teamId string, userId string, terms string, isHasht
var posts []*model.Post
if utils.Cfg.SqlSettings.DriverName == "postgres" {
+
+ // Parse text for wildcards
+ if wildcard, err := regexp.Compile("\\*($| )"); err == nil {
+ terms = wildcard.ReplaceAllLiteralString(terms, ":* ")
+ }
+
searchQuery := fmt.Sprintf(`SELECT
*
FROM
diff --git a/store/sql_post_store_test.go b/store/sql_post_store_test.go
index 4564e2deb..d48dea51c 100644
--- a/store/sql_post_store_test.go
+++ b/store/sql_post_store_test.go
@@ -4,11 +4,11 @@
package store
import (
- "github.com/mattermost/platform/model"
- "github.com/mattermost/platform/utils"
"strings"
"testing"
"time"
+
+ "github.com/mattermost/platform/model"
)
func TestPostStoreSave(t *testing.T) {
@@ -547,11 +547,9 @@ func TestPostStoreSearch(t *testing.T) {
t.Fatal("returned wrong serach result")
}
- if utils.Cfg.SqlSettings.DriverName == "mysql" {
- r5 := (<-store.Post().Search(teamId, userId, "matter*", false)).Data.(*model.PostList)
- if len(r5.Order) != 1 && r5.Order[0] != o1.Id {
- t.Fatal("returned wrong serach result")
- }
+ r5 := (<-store.Post().Search(teamId, userId, "matter*", false)).Data.(*model.PostList)
+ if len(r5.Order) != 1 && r5.Order[0] != o1.Id {
+ t.Fatal("returned wrong serach result")
}
r6 := (<-store.Post().Search(teamId, userId, "#hashtag", true)).Data.(*model.PostList)
@@ -573,4 +571,9 @@ func TestPostStoreSearch(t *testing.T) {
if len(r9.Order) != 2 {
t.Fatal("returned wrong search result")
}
+
+ r10 := (<-store.Post().Search(teamId, userId, "matter* jer*", false)).Data.(*model.PostList)
+ if len(r10.Order) != 2 {
+ t.Fatal("returned wrong search result")
+ }
}
diff --git a/web/react/.eslintrc b/web/react/.eslintrc
index cdf96905b..ab000e941 100644
--- a/web/react/.eslintrc
+++ b/web/react/.eslintrc
@@ -39,7 +39,7 @@
"no-unexpected-multiline": 2,
"block-scoped-var": 1,
- "complexity": [1, 8],
+ "complexity": [0, 8],
"consistent-return": 2,
"curly": [2, "all"],
"dot-notation": 2,
@@ -97,7 +97,7 @@
// Style
"array-bracket-spacing": [2, "never"],
"brace-style": [2, "1tbs", { "allowSingleLine": false }],
- "camelcase": [2, {"properties": "always"}],
+ "camelcase": [2, {"properties": "never"}],
"comma-spacing": [2, {"before": false, "after": true}],
"comma-style": [2, "last"],
"computed-property-spacing": [2, "never"],
diff --git a/web/react/components/command_list.jsx b/web/react/components/command_list.jsx
index 5efe98dc6..27264ff6e 100644
--- a/web/react/components/command_list.jsx
+++ b/web/react/components/command_list.jsx
@@ -48,15 +48,15 @@ module.exports = React.createClass({
if (this.state.suggestions[i].suggestion != this.state.cmd) {
suggestions.push(
<div key={i} className="command-name" onClick={this.handleClick.bind(this, i)}>
- <div className="pull-left"><strong>{ this.state.suggestions[i].suggestion }</strong></div>
- <div className="command-desc pull-right">{ this.state.suggestions[i].description }</div>
+ <div className="command__title"><strong>{ this.state.suggestions[i].suggestion }</strong></div>
+ <div className="command__desc">{ this.state.suggestions[i].description }</div>
</div>
);
}
}
return (
- <div ref="mentionlist" className="command-box" style={{height:(this.state.suggestions.length*37)+2}}>
+ <div ref="mentionlist" className="command-box" style={{height:(this.state.suggestions.length*56)+2}}>
{ suggestions }
</div>
);
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index 73e897f62..c80b287a3 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -43,13 +43,16 @@ export default class PostInfo extends React.Component {
if (isOwner) {
dropdownContents.push(
- <li role='presentation'>
+ <li
+ key='editPost'
+ role='presentation'
+ >
<a
href='#'
role='menuitem'
data-toggle='modal'
data-target='#edit_post'
- data-refoucsid="#post_textbox"
+ data-refoucsid='#post_textbox'
data-title={type}
data-message={post.message}
data-postid={post.id}
@@ -64,7 +67,10 @@ export default class PostInfo extends React.Component {
if (isOwner || isAdmin) {
dropdownContents.push(
- <li role='presentation'>
+ <li
+ key='deletePost'
+ role='presentation'
+ >
<a
href='#'
role='menuitem'
@@ -83,7 +89,10 @@ export default class PostInfo extends React.Component {
if (this.props.allowReply === 'true') {
dropdownContents.push(
- <li role='presentation'>
+ <li
+ key='replyLink'
+ role='presentation'
+ >
<a
className='reply-link theme'
href='#'
diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx
index 5b0b1f79a..c1e6e490d 100644
--- a/web/react/components/post_list.jsx
+++ b/web/react/components/post_list.jsx
@@ -401,6 +401,14 @@ export default class PostList extends React.Component {
>
<i className='fa fa-pencil'></i>Set a description
</a>
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#channel_invite'
+ >
+ <i className='fa fa-user-plus'></i>Invite others to this channel
+ </a>
</div>
);
}
diff --git a/web/react/components/setting_item_max.jsx b/web/react/components/setting_item_max.jsx
index 1599041b0..b978cdb0c 100644
--- a/web/react/components/setting_item_max.jsx
+++ b/web/react/components/setting_item_max.jsx
@@ -5,6 +5,7 @@ module.exports = React.createClass({
render: function() {
var clientError = this.props.client_error ? <div className='form-group'><label className='col-sm-12 has-error'>{ this.props.client_error }</label></div> : null;
var server_error = this.props.server_error ? <div className='form-group'><label className='col-sm-12 has-error'>{ this.props.server_error }</label></div> : null;
+ var extraInfo = this.props.extraInfo ? this.props.extraInfo : null;
var inputs = this.props.inputs;
@@ -15,6 +16,7 @@ module.exports = React.createClass({
<ul className="setting-list">
<li className="setting-list-item">
{inputs}
+ {extraInfo}
</li>
<li className="setting-list-item">
<hr />
diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx
index d5d16816f..af65b7e1d 100644
--- a/web/react/components/sidebar_header.jsx
+++ b/web/react/components/sidebar_header.jsx
@@ -85,7 +85,7 @@ var NavbarDropdown = React.createClass({
}
});
}
- teams.push(<li key='newTeam_li'><a key='newTeam_a' href={utils.getWindowLocationOrigin() + '/signup_team' }>Create a New Team</a></li>);
+ teams.push(<li key='newTeam_li'><a key='newTeam_a' target="_blank" href={utils.getWindowLocationOrigin() + '/signup_team' }>Create a New Team</a></li>);
return (
<ul className='nav navbar-nav navbar-right'>
diff --git a/web/react/components/user_settings_general.jsx b/web/react/components/user_settings_general.jsx
index fed11fbe9..ddd2fb607 100644
--- a/web/react/components/user_settings_general.jsx
+++ b/web/react/components/user_settings_general.jsx
@@ -11,10 +11,32 @@ var AsyncClient = require('../utils/async_client.jsx');
var utils = require('../utils/utils.jsx');
var assign = require('object-assign');
-module.exports = React.createClass({
- displayName: 'GeneralTab',
- submitActive: false,
- submitUsername: function(e) {
+export default class UserSettingsGeneralTab extends React.Component {
+ constructor(props) {
+ super(props);
+ this.submitActive = false;
+
+ this.submitUsername = this.submitUsername.bind(this);
+ this.submitNickname = this.submitNickname.bind(this);
+ this.submitName = this.submitName.bind(this);
+ this.submitEmail = this.submitEmail.bind(this);
+ this.submitUser = this.submitUser.bind(this);
+ this.submitPicture = this.submitPicture.bind(this);
+
+ this.updateUsername = this.updateUsername.bind(this);
+ this.updateFirstName = this.updateFirstName.bind(this);
+ this.updateLastName = this.updateLastName.bind(this);
+ this.updateNickname = this.updateNickname.bind(this);
+ this.updateEmail = this.updateEmail.bind(this);
+ this.updatePicture = this.updatePicture.bind(this);
+ this.updateSection = this.updateSection.bind(this);
+
+ this.handleClose = this.handleClose.bind(this);
+ this.setupInitialState = this.setupInitialState.bind(this);
+
+ this.state = this.setupInitialState(props);
+ }
+ submitUsername(e) {
e.preventDefault();
var user = this.props.user;
@@ -37,8 +59,8 @@ module.exports = React.createClass({
user.username = username;
this.submitUser(user);
- },
- submitNickname: function(e) {
+ }
+ submitNickname(e) {
e.preventDefault();
var user = UserStore.getCurrentUser();
@@ -52,8 +74,8 @@ module.exports = React.createClass({
user.nickname = nickname;
this.submitUser(user);
- },
- submitName: function(e) {
+ }
+ submitName(e) {
e.preventDefault();
var user = UserStore.getCurrentUser();
@@ -69,8 +91,8 @@ module.exports = React.createClass({
user.last_name = lastName;
this.submitUser(user);
- },
- submitEmail: function(e) {
+ }
+ submitEmail(e) {
e.preventDefault();
var user = UserStore.getCurrentUser();
@@ -88,15 +110,15 @@ module.exports = React.createClass({
user.email = email;
this.submitUser(user);
- },
- submitUser: function(user) {
+ }
+ submitUser(user) {
client.updateUser(user,
- function() {
+ function updateSuccess() {
this.updateSection('');
AsyncClient.getMe();
}.bind(this),
- function(err) {
- var state = this.getInitialState();
+ function updateFailure(err) {
+ var state = this.setupInitialState(this.props);
if (err.message) {
state.serverError = err.message;
} else {
@@ -105,8 +127,8 @@ module.exports = React.createClass({
this.setState(state);
}.bind(this)
);
- },
- submitPicture: function(e) {
+ }
+ submitPicture(e) {
e.preventDefault();
if (!this.state.picture) {
@@ -129,34 +151,34 @@ module.exports = React.createClass({
this.setState({loadingPicture: true});
client.uploadProfileImage(formData,
- function() {
+ function imageUploadSuccess() {
this.submitActive = false;
AsyncClient.getMe();
window.location.reload();
}.bind(this),
- function(err) {
- var state = this.getInitialState();
+ function imageUploadFailure(err) {
+ var state = this.setupInitialState(this.props);
state.serverError = err;
this.setState(state);
}.bind(this)
);
- },
- updateUsername: function(e) {
+ }
+ updateUsername(e) {
this.setState({username: e.target.value});
- },
- updateFirstName: function(e) {
+ }
+ updateFirstName(e) {
this.setState({firstName: e.target.value});
- },
- updateLastName: function(e) {
+ }
+ updateLastName(e) {
this.setState({lastName: e.target.value});
- },
- updateNickname: function(e) {
+ }
+ updateNickname(e) {
this.setState({nickname: e.target.value});
- },
- updateEmail: function(e) {
+ }
+ updateEmail(e) {
this.setState({email: e.target.value});
- },
- updatePicture: function(e) {
+ }
+ updatePicture(e) {
if (e.target.files && e.target.files[0]) {
this.setState({picture: e.target.files[0]});
@@ -165,34 +187,33 @@ module.exports = React.createClass({
} else {
this.setState({picture: null});
}
- },
- updateSection: function(section) {
- this.setState(assign({}, this.getInitialState(), {clientError: ''}));
+ }
+ updateSection(section) {
+ this.setState(assign({}, this.setupInitialState(this.props), {clientError: '', serverError: '', emailError: ''}));
this.submitActive = false;
this.props.updateSection(section);
- },
- handleClose: function() {
- $(this.getDOMNode()).find('.form-control').each(function() {
+ }
+ handleClose() {
+ $(this.getDOMNode()).find('.form-control').each(function clearForms() {
this.value = '';
});
- this.setState(assign({}, this.getInitialState(), {clientError: null, serverError: null, emailError: null}));
+ this.setState(assign({}, this.setupInitialState(this.props), {clientError: null, serverError: null, emailError: null}));
this.props.updateSection('');
- },
- componentDidMount: function() {
+ }
+ componentDidMount() {
$('#user_settings').on('hidden.bs.modal', this.handleClose);
- },
- componentWillUnmount: function() {
+ }
+ componentWillUnmount() {
$('#user_settings').off('hidden.bs.modal', this.handleClose);
- },
- getInitialState: function() {
- var user = this.props.user;
+ }
+ setupInitialState(props) {
+ var user = props.user;
var emailEnabled = !ConfigStore.getSettingAsBoolean('ByPassEmail', false);
-
return {username: user.username, firstName: user.first_name, lastName: user.last_name, nickname: user.nickname,
- email: user.email, picture: null, loadingPicture: false, emailEnabled: emailEnabled};
- },
- render: function() {
+ email: user.email, picture: null, loadingPicture: false, emailEnabled: emailEnabled};
+ }
+ render() {
var user = this.props.user;
var clientError = null;
@@ -214,19 +235,35 @@ module.exports = React.createClass({
if (this.props.activeSection === 'name') {
inputs.push(
- <div className='form-group'>
+ <div
+ key='firstNameSetting'
+ className='form-group'
+ >
<label className='col-sm-5 control-label'>First Name</label>
<div className='col-sm-7'>
- <input className='form-control' type='text' onChange={this.updateFirstName} value={this.state.firstName}/>
+ <input
+ className='form-control'
+ type='text'
+ onChange={this.updateFirstName}
+ value={this.state.firstName}
+ />
</div>
</div>
);
inputs.push(
- <div className='form-group'>
+ <div
+ key='lastNameSetting'
+ className='form-group'
+ >
<label className='col-sm-5 control-label'>Last Name</label>
<div className='col-sm-7'>
- <input className='form-control' type='text' onChange={this.updateLastName} value={this.state.lastName}/>
+ <input
+ className='form-control'
+ type='text'
+ onChange={this.updateLastName}
+ value={this.state.lastName}
+ />
</div>
</div>
);
@@ -238,7 +275,7 @@ module.exports = React.createClass({
submit={this.submitName}
server_error={serverError}
client_error={clientError}
- updateSection={function(e) {
+ updateSection={function clearSection(e) {
self.updateSection('');
e.preventDefault();
}}
@@ -259,7 +296,7 @@ module.exports = React.createClass({
<SettingItemMin
title='Full Name'
describe={fullName}
- updateSection={function() {
+ updateSection={function updateNameSection() {
self.updateSection('name');
}}
/>
@@ -268,11 +305,24 @@ module.exports = React.createClass({
var nicknameSection;
if (this.props.activeSection === 'nickname') {
+ let nicknameLabel = 'Nickname';
+ if (utils.isMobile()) {
+ nicknameLabel = '';
+ }
+
inputs.push(
- <div className='form-group'>
- <label className='col-sm-5 control-label'>{utils.isMobile() ? '' : 'Nickname'}</label>
+ <div
+ key='nicknameSetting'
+ className='form-group'
+ >
+ <label className='col-sm-5 control-label'>{nicknameLabel}</label>
<div className='col-sm-7'>
- <input className='form-control' type='text' onChange={this.updateNickname} value={this.state.nickname}/>
+ <input
+ className='form-control'
+ type='text'
+ onChange={this.updateNickname}
+ value={this.state.nickname}
+ />
</div>
</div>
);
@@ -284,7 +334,7 @@ module.exports = React.createClass({
submit={this.submitNickname}
server_error={serverError}
client_error={clientError}
- updateSection={function(e) {
+ updateSection={function clearSection(e) {
self.updateSection('');
e.preventDefault();
}}
@@ -295,7 +345,7 @@ module.exports = React.createClass({
<SettingItemMin
title='Nickname'
describe={UserStore.getCurrentUser().nickname}
- updateSection={function() {
+ updateSection={function updateNicknameSection() {
self.updateSection('nickname');
}}
/>
@@ -304,11 +354,24 @@ module.exports = React.createClass({
var usernameSection;
if (this.props.activeSection === 'username') {
+ let usernameLabel = 'Username';
+ if (utils.isMobile()) {
+ usernameLabel = '';
+ }
+
inputs.push(
- <div className='form-group'>
- <label className='col-sm-5 control-label'>{utils.isMobile() ? '' : 'Username'}</label>
+ <div
+ key='usernameSetting'
+ className='form-group'
+ >
+ <label className='col-sm-5 control-label'>{usernameLabel}</label>
<div className='col-sm-7'>
- <input className='form-control' type='text' onChange={this.updateUsername} value={this.state.username}/>
+ <input
+ className='form-control'
+ type='text'
+ onChange={this.updateUsername}
+ value={this.state.username}
+ />
</div>
</div>
);
@@ -320,7 +383,7 @@ module.exports = React.createClass({
submit={this.submitUsername}
server_error={serverError}
client_error={clientError}
- updateSection={function(e) {
+ updateSection={function clearSection(e) {
self.updateSection('');
e.preventDefault();
}}
@@ -331,7 +394,7 @@ module.exports = React.createClass({
<SettingItemMin
title='Username'
describe={UserStore.getCurrentUser().username}
- updateSection={function() {
+ updateSection={function updateUsernameSection() {
self.updateSection('username');
}}
/>
@@ -346,11 +409,16 @@ module.exports = React.createClass({
}
inputs.push(
- <div>
+ <div key='emailSetting'>
<div className='form-group'>
<label className='col-sm-5 control-label'>Primary Email</label>
<div className='col-sm-7'>
- <input className='form-control' type='text' onChange={this.updateEmail} value={this.state.email}/>
+ <input
+ className='form-control'
+ type='text'
+ onChange={this.updateEmail}
+ value={this.state.email}
+ />
</div>
</div>
{helpText}
@@ -364,7 +432,7 @@ module.exports = React.createClass({
submit={this.submitEmail}
server_error={serverError}
client_error={emailError}
- updateSection={function(e) {
+ updateSection={function clearSection(e) {
self.updateSection('');
e.preventDefault();
}}
@@ -375,7 +443,7 @@ module.exports = React.createClass({
<SettingItemMin
title='Email'
describe={UserStore.getCurrentUser().email}
- updateSection={function() {
+ updateSection={function updateEmailSection() {
self.updateSection('email');
}}
/>
@@ -391,7 +459,7 @@ module.exports = React.createClass({
src={'/api/v1/users/' + user.id + '/image?time=' + user.last_picture_update}
server_error={serverError}
client_error={clientError}
- updateSection={function(e) {
+ updateSection={function clearSection(e) {
self.updateSection('');
e.preventDefault();
}}
@@ -410,7 +478,7 @@ module.exports = React.createClass({
<SettingItemMin
title='Profile Picture'
describe={minMessage}
- updateSection={function() {
+ updateSection={function updatePictureSection() {
self.updateSection('picture');
}}
/>
@@ -419,8 +487,21 @@ module.exports = React.createClass({
return (
<div>
<div className='modal-header'>
- <button type='button' className='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>&times;</span></button>
- <h4 className='modal-title' ref='title'><i className='modal-back'></i>General Settings</h4>
+ <button
+ type='button'
+ className='close'
+ data-dismiss='modal'
+ aria-label='Close'
+ >
+ <span aria-hidden='true'>&times;</span>
+ </button>
+ <h4
+ className='modal-title'
+ ref='title'
+ >
+ <i className='modal-back'></i>
+ General Settings
+ </h4>
</div>
<div className='user-settings'>
<h3 className='tab-header'>General Settings</h3>
@@ -439,4 +520,10 @@ module.exports = React.createClass({
</div>
);
}
-});
+}
+
+UserSettingsGeneralTab.propTypes = {
+ user: React.PropTypes.object,
+ updateSection: React.PropTypes.func,
+ activeSection: React.PropTypes.string
+};
diff --git a/web/react/components/user_settings_notifications.jsx b/web/react/components/user_settings_notifications.jsx
index b89f72987..ba0bda78e 100644
--- a/web/react/components/user_settings_notifications.jsx
+++ b/web/react/components/user_settings_notifications.jsx
@@ -1,6 +1,7 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
+var ChannelStore = require('../stores/channel_store.jsx');
var UserStore = require('../stores/user_store.jsx');
var SettingItemMin = require('./setting_item_min.jsx');
var SettingItemMax = require('./setting_item_max.jsx');
@@ -67,7 +68,11 @@ function getNotificationsStateFromStores() {
}
}
- return {notifyLevel: desktop, enableEmail: email, soundNeeded: soundNeeded, enableSound: sound, usernameKey: usernameKey, mentionKey: mentionKey, customKeys: customKeys, customKeysChecked: customKeys.length > 0, firstNameKey: firstNameKey, allKey: allKey, channelKey: channelKey};
+ var curChannel = ChannelStore.getCurrent().display_name;
+
+ return {notifyLevel: desktop, enableEmail: email, soundNeeded: soundNeeded, enableSound: sound,
+ usernameKey: usernameKey, mentionKey: mentionKey, customKeys: customKeys, customKeysChecked: customKeys.length > 0,
+ firstNameKey: firstNameKey, allKey: allKey, channelKey: channelKey, curChannel: curChannel};
}
export default class NotificationsTab extends React.Component {
@@ -141,10 +146,12 @@ export default class NotificationsTab extends React.Component {
}
componentDidMount() {
UserStore.addChangeListener(this.onListenerChange);
+ ChannelStore.addChangeListener(this.onListenerChange);
$('#user_settings').on('hidden.bs.modal', this.handleClose);
}
componentWillUnmount() {
UserStore.removeChangeListener(this.onListenerChange);
+ ChannelStore.removeChangeListener(this.onListenerChange);
$('#user_settings').off('hidden.bs.modal', this.handleClose);
this.props.updateSection('');
}
@@ -265,6 +272,12 @@ export default class NotificationsTab extends React.Component {
e.preventDefault();
};
+ let extraInfo = (
+ <div className='setting-list__hint'>
+ These settings will override the global notification settings for the <b>{this.state.curChannel}</b> channel
+ </div>
+ )
+
desktopSection = (
<SettingItemMax
title='Send desktop notifications'
@@ -272,6 +285,7 @@ export default class NotificationsTab extends React.Component {
submit={this.handleSubmit}
server_error={serverError}
updateSection={handleUpdateDesktopSection}
+ extraInfo={extraInfo}
/>
);
} else {
diff --git a/web/react/stores/channel_store.jsx b/web/react/stores/channel_store.jsx
index f7c23841c..678d50bbd 100644
--- a/web/react/stores/channel_store.jsx
+++ b/web/react/stores/channel_store.jsx
@@ -270,4 +270,5 @@ ChannelStore.dispatchToken = AppDispatcher.register(function(payload) {
}
});
+ChannelStore.setMaxListeners(11);
module.exports = ChannelStore;
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 5266b1381..a1dc72ae2 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -260,10 +260,40 @@ module.exports.escapeRegExp = function(string) {
return string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1');
};
+function handleYoutubeTime(link) {
+ var timeRegex = /[\\?&]t=([0-9hms]+)/;
+
+ var time = link.trim().match(timeRegex);
+ if (!time || !time[1]) {
+ return '';
+ }
+
+ var hours = time[1].match(/([0-9]+)h/);
+ var minutes = time[1].match(/([0-9]+)m/);
+ var seconds = time[1].match(/([0-9]+)s/);
+
+ var ticks = 0;
+
+ if (hours && hours[1]) {
+ ticks += parseInt(hours[1], 10) * 3600;
+ }
+
+ if (minutes && minutes[1]) {
+ ticks += parseInt(minutes[1], 10) * 60;
+ }
+
+ if (seconds && seconds[1]) {
+ ticks += parseInt(seconds[1], 10);
+ }
+
+ return '&start=' + ticks.toString();
+}
+
function getYoutubeEmbed(link) {
var regex = /.*(?:youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|watch\?(?:[a-zA-Z-_]+=[a-zA-Z0-9-_]+&)+v=)([^#\&\?]*).*/;
var youtubeId = link.trim().match(regex)[1];
+ var time = handleYoutubeTime(link);
function onClick(e) {
var div = $(e.target).closest('.video-thumbnail__container')[0];
@@ -271,7 +301,8 @@ function getYoutubeEmbed(link) {
iframe.setAttribute('src',
'https://www.youtube.com/embed/' +
div.id +
- '?autoplay=1&autohide=1&border=0&wmode=opaque&fs=1&enablejsapi=1');
+ '?autoplay=1&autohide=1&border=0&wmode=opaque&fs=1&enablejsapi=1' +
+ time);
iframe.setAttribute('width', '480px');
iframe.setAttribute('height', '360px');
iframe.setAttribute('type', 'text/html');
@@ -286,6 +317,7 @@ function getYoutubeEmbed(link) {
return;
}
var metadata = data.items[0].snippet;
+ $('.video-type.' + youtubeId).html("Youtube - ")
$('.video-uploader.' + youtubeId).html(metadata.channelTitle);
$('.video-title.' + youtubeId).find('a').html(metadata.title);
$('.post-list-holder-by-time').scrollTop($('.post-list-holder-by-time')[0].scrollHeight);
@@ -303,9 +335,11 @@ function getYoutubeEmbed(link) {
return (
<div className='post-comment'>
- <h4 className='video-type'>YouTube</h4>
+ <h4>
+ <span className={'video-type ' + youtubeId}>YouTube</span>
+ <span className={'video-title ' + youtubeId}><a href={link}></a></span>
+ </h4>
<h4 className={'video-uploader ' + youtubeId}></h4>
- <h4 className={'video-title ' + youtubeId}><a href={link}></a></h4>
<div className='video-div embed-responsive-item' id={youtubeId} onClick={onClick}>
<div className='embed-responsive embed-responsive-4by3 video-div__placeholder'>
<div id={youtubeId} className='video-thumbnail__container'>
@@ -456,9 +490,21 @@ module.exports.textToJsx = function(text, options) {
var mentionRegex = /^(?:@)([a-z0-9_]+)$/gi; // looks loop invariant but a weird JS bug needs it to be redefined here
var explicitMention = mentionRegex.exec(trimWord);
- if ((trimWord.toLowerCase().indexOf(searchTerm) > -1 || word.toLowerCase().indexOf(searchTerm) > -1) && searchTerm != '') {
-
- highlightSearchClass = ' search-highlight';
+ if (searchTerm !== '') {
+ let searchWords = searchTerm.split(' ');
+ for (let idx in searchWords) {
+ let searchWord = searchWords[idx];
+ if (searchWord === word.toLowerCase() || searchWord === trimWord.toLowerCase()) {
+ highlightSearchClass = ' search-highlight';
+ break;
+ } else if (searchWord.charAt(searchWord.length - 1) === '*') {
+ let searchWordPrefix = searchWord.slice(0,-1);
+ if (trimWord.toLowerCase().indexOf(searchWordPrefix) > -1 || word.toLowerCase().indexOf(searchWordPrefix) > -1) {
+ highlightSearchClass = ' search-highlight';
+ break;
+ }
+ }
+ }
}
if (explicitMention &&
diff --git a/web/sass-files/sass/partials/_command-box.scss b/web/sass-files/sass/partials/_command-box.scss
index 44eb9b8df..565296fae 100644
--- a/web/sass-files/sass/partials/_command-box.scss
+++ b/web/sass-files/sass/partials/_command-box.scss
@@ -4,20 +4,30 @@
width: 100%;
border: $border-gray;
bottom: 38px;
+ overflow: auto;
@extend %popover-box-shadow;
+ .sidebar--right & {
+ bottom: 100px;
+ }
}
.command-name {
position: relative;
width: 100%;
background-color: #fff;
- height: 37px;
- line-height: 37px;
- padding: 2px 10px 2px 5px;
+ line-height: 24px;
+ padding: 5px 10px 8px;
z-index: 101;
+ font-size: 0.95em;
+ border-bottom: 1px solid #ddd;
&:hover {
background-color: #e8eaed;
}
+ .command__desc {
+ margin-left: 5px;
+ color: #999;
+ line-height: normal;
+ }
}
.command-desc {
diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss
index 99a7eb7bc..8d743dfe2 100644
--- a/web/sass-files/sass/partials/_settings.scss
+++ b/web/sass-files/sass/partials/_settings.scss
@@ -94,6 +94,11 @@
list-style-type:none;
}
+ .setting-list__hint {
+ color: #555;
+ margin-top: 20px;
+ }
+
.mentions-input {
margin-top: 10px;
}
diff --git a/web/templates/head.html b/web/templates/head.html
index c27597f82..6b6b7f09d 100644
--- a/web/templates/head.html
+++ b/web/templates/head.html
@@ -7,7 +7,7 @@
<!-- iOS add to homescreen -->
<meta name="apple-mobile-web-app-capable" content="yes" />
- <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
+ <meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="{{ .Title }}">
<meta name="application-name" content="{{ .Title }}">