summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md74
-rw-r--r--Makefile4
-rw-r--r--NOTICE.txt37
-rw-r--r--api/auto_environment.go (renamed from api/auto_enviroment.go)30
-rw-r--r--api/auto_teams.go2
-rw-r--r--api/command.go48
-rw-r--r--config/config.json2
-rw-r--r--doc/install/Amazon-Elastic-Beanstalk.md45
-rw-r--r--doc/install/Requirements.md21
-rw-r--r--doc/install/SMTP-Email-Setup.md12
-rw-r--r--doc/integrations/webhooks/Incoming-Webhooks.md6
-rw-r--r--doc/integrations/webhooks/Outgoing-Webhooks.md2
-rw-r--r--doc/process/documentation-guidelines.md185
-rw-r--r--manualtesting/manual_testing.go6
-rw-r--r--manualtesting/test_autolink.go2
-rw-r--r--model/outgoing_webhook.go6
-rw-r--r--model/outgoing_webhook_test.go5
-rw-r--r--model/team.go1
-rw-r--r--model/utils.go13
-rw-r--r--store/sql_audit_store.go4
-rw-r--r--store/sql_channel_store.go6
-rw-r--r--store/sql_oauth_store.go10
-rw-r--r--store/sql_post_store.go9
-rw-r--r--store/sql_post_store_test.go12
-rw-r--r--store/sql_preference_store.go6
-rw-r--r--store/sql_session_store.go8
-rw-r--r--store/sql_store.go5
-rw-r--r--store/sql_system_store.go6
-rw-r--r--store/sql_team_store.go8
-rw-r--r--store/sql_user_store.go12
-rw-r--r--web/react/components/admin_console/gitlab_settings.jsx1
-rw-r--r--web/react/components/center_panel.jsx54
-rw-r--r--web/react/components/channel_header.jsx7
-rw-r--r--web/react/components/channel_view.jsx43
-rw-r--r--web/react/components/create_post.jsx1
-rw-r--r--web/react/components/edit_post_modal.jsx2
-rw-r--r--web/react/components/find_team.jsx2
-rw-r--r--web/react/components/post.jsx4
-rw-r--r--web/react/components/post_body.jsx4
-rw-r--r--web/react/components/post_info.jsx20
-rw-r--r--web/react/components/post_list.jsx759
-rw-r--r--web/react/components/post_list_container.jsx63
-rw-r--r--web/react/components/posts_view.jsx297
-rw-r--r--web/react/components/posts_view_container.jsx264
-rw-r--r--web/react/components/rhs_thread.jsx10
-rw-r--r--web/react/components/search_autocomplete.jsx5
-rw-r--r--web/react/components/search_bar.jsx16
-rw-r--r--web/react/components/sidebar.jsx19
-rw-r--r--web/react/components/sidebar_right.jsx61
-rw-r--r--web/react/components/team_signup_display_name_page.jsx3
-rw-r--r--web/react/components/team_signup_url_page.jsx4
-rw-r--r--web/react/components/time_since.jsx50
-rw-r--r--web/react/components/user_settings/manage_outgoing_hooks.jsx38
-rw-r--r--web/react/components/user_settings/user_settings_display.jsx100
-rw-r--r--web/react/components/user_settings/user_settings_integrations.jsx2
-rw-r--r--web/react/components/user_settings/user_settings_notifications.jsx12
-rw-r--r--web/react/pages/channel.jsx93
-rw-r--r--web/react/stores/post_store.jsx43
-rw-r--r--web/react/stores/socket_store.jsx6
-rw-r--r--web/react/utils/channel_intro_mssages.jsx218
-rw-r--r--web/react/utils/constants.jsx5
-rw-r--r--web/react/utils/utils.jsx19
-rw-r--r--web/sass-files/sass/partials/_base.scss38
-rw-r--r--web/sass-files/sass/partials/_headers.scss3
-rw-r--r--web/sass-files/sass/partials/_post.scss3
-rw-r--r--web/sass-files/sass/partials/_responsive.scss2
-rw-r--r--web/sass-files/sass/partials/_search.scss5
-rw-r--r--web/static/help/about.html2
-rw-r--r--web/templates/channel.html19
-rw-r--r--web/web.go4
70 files changed, 1723 insertions, 1165 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8082b2536..46b74ccb3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,9 +7,79 @@ The "UNDER DEVELOPMENT" section of the Mattermost changelog appears in the produ
- **Release candidate anticipated:** 2015-11-10
- **Final release anticipated:** 2015-11-16
-### Changes
+### Improvements
-- IE 10 no longer supported since global share of IE 10 fell below 5%
+Onboarding
+
+- New tutorial explaining how to use Mattermost for new users
+
+Messaging and Notifications
+
+- Users can now search for teammates to add to **Direct Message** list via **More** menu
+- Users can now personalize Direct Messages list by removing users listed
+- Link previews - Adding URL with .gif file adds image below message
+
+Search
+
+- Adding search term `in:[channel_url_name]` now limits searches within a specific channel
+- Adding search term `from:[username]` now limits searches to messages from a specific user
+- Tip explaining search options when clicking into search box
+
+Integrations
+
+- [Outgoing webhooks](https://github.com/mattermost/platform/blob/master/doc/integrations/webhooks/Outgoing-Webhooks.md) now available
+- Made available [application template showing outgoing webhooks working with Mattermost and external application](https://github.com/mattermost/mattermost-integration-giphy)
+
+User Interface
+
+- Member list in Channel display now scrollable, and includes Message button to message channel members directly
+- Added ability to edit previous message by hitting UP arrow
+- Syntax highlighting added for code blocks
+ - Languages include `Diff, Apache, Makefile, HTTP, JSON, Markdown, Java, CSS, nginx, ObjectiveC, Python, XML, Perl, Bash, PHP, Coffee, C, SQL, Go, Ruby, Java, and ini`.
+ - Use by adding the name of the language on the first link of the code block, for example: ```python
+ - Syntax color theme can be defined under **Account Settings** > **Appearance Settings** > **Custom Theme**
+
+Team Settings
+
+- Added Team Settings option to include account creation URL on team login page
+- Added Team Settings option to include link to given team on root page
+- Ability to rotate invite code for invite URL
+
+System Console
+
+- New statistics page
+- Configurable option to create an account directly from team page
+
+#### Bug Fixes
+
+- Various fixes to theme colors
+
+### Compatibility
+
+- IE 11 new minimum version for IE, since IE 10 share fell below 5% on desktop
+- Safari 8 new minimum version for Safari, since Safari 7 fell below 1% on desktop
+
+#### Known Issues
+
+- Microsoft Edge does not yet support drag and drop
+- Incoming webhooks no longer disrupted when channel is deleted
+
+#### Contributors
+
+Many thanks to our external contributors. In no particular order:
+
+- [florianorben](https://github.com/florianorben)
+- [trashcan](https://github.com/trashcan)
+- [girishso](https://github.com/girishso)
+- [apaatsio](https://github.com/apaatsio)
+- [jlebleu](https://github.com/jlebleu)
+- [stasvovk](https://github.com/stasvovk)
+- [mcmillhj](https://github.com/mcmillhj)
+- [sharms](https://github.com/sharms)
+- [jvasallo](https://github.com/jvasallo)
+- [layzerar](https://github.com/layzerar)
+- [optimistiks](https://github.com/optimistiks)
+- [layzerar](https://github.com/layzerar)
## Release v1.1.1 (Bug Fix Release)
diff --git a/Makefile b/Makefile
index 5e798ee76..573036f06 100644
--- a/Makefile
+++ b/Makefile
@@ -46,7 +46,7 @@ travis:
cd web/react/ && npm run build-libs
@echo Checking for style guide compliance
- cd web/react && $(ESLINT) --quiet components/* dispatcher/* pages/* stores/* utils/*
+ cd web/react && $(ESLINT) --ext ".jsx" --ignore-pattern node_modules --quiet .
@echo Running gofmt
$(eval GOFMT_OUTPUT := $(shell gofmt -d -s api/ model/ store/ utils/ manualtesting/ mattermost.go 2>&1))
@echo "$(GOFMT_OUTPUT)"
@@ -143,7 +143,7 @@ install:
check: install
@echo Running ESLint...
- -cd web/react && $(ESLINT) components/* dispatcher/* pages/* stores/* utils/*
+ -cd web/react && $(ESLINT) --ext ".jsx" --ignore-pattern node_modules .
@echo Running gofmt
$(eval GOFMT_OUTPUT := $(shell gofmt -d -s api/ model/ store/ utils/ manualtesting/ mattermost.go 2>&1))
@echo "$(GOFMT_OUTPUT)"
diff --git a/NOTICE.txt b/NOTICE.txt
index cc9e35af8..c43fc2d22 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -921,3 +921,40 @@ distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+
+---
+
+This product contains a modified portion of 'highlight.js', a syntax highlighter for the web.
+
+by Ivan Sagalaev
+
+* HOMEPAGE:
+ * https://github.com/isagalaev/highlight.js
+
+* LICENSE:
+
+Copyright (c) 2006, Ivan Sagalaev
+All rights reserved.
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of highlight.js nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
diff --git a/api/auto_enviroment.go b/api/auto_environment.go
index c6453f5da..68186ec6c 100644
--- a/api/auto_enviroment.go
+++ b/api/auto_environment.go
@@ -10,42 +10,42 @@ import (
"time"
)
-type TestEnviroment struct {
- Teams []*model.Team
- Enviroments []TeamEnviroment
+type TestEnvironment struct {
+ Teams []*model.Team
+ Environments []TeamEnvironment
}
-func CreateTestEnviromentWithTeams(client *model.Client, rangeTeams utils.Range, rangeChannels utils.Range, rangeUsers utils.Range, rangePosts utils.Range, fuzzy bool) (TestEnviroment, bool) {
+func CreateTestEnvironmentWithTeams(client *model.Client, rangeTeams utils.Range, rangeChannels utils.Range, rangeUsers utils.Range, rangePosts utils.Range, fuzzy bool) (TestEnvironment, bool) {
rand.Seed(time.Now().UTC().UnixNano())
teamCreator := NewAutoTeamCreator(client)
teamCreator.Fuzzy = fuzzy
teams, err := teamCreator.CreateTestTeams(rangeTeams)
if err != true {
- return TestEnviroment{}, false
+ return TestEnvironment{}, false
}
- enviroment := TestEnviroment{teams, make([]TeamEnviroment, len(teams))}
+ environment := TestEnvironment{teams, make([]TeamEnvironment, len(teams))}
for i, team := range teams {
userCreator := NewAutoUserCreator(client, team.Id)
userCreator.Fuzzy = fuzzy
randomUser, err := userCreator.createRandomUser()
if err != true {
- return TestEnviroment{}, false
+ return TestEnvironment{}, false
}
client.LoginById(randomUser.Id, USER_PASSWORD)
- teamEnviroment, err := CreateTestEnviromentInTeam(client, team.Id, rangeChannels, rangeUsers, rangePosts, fuzzy)
+ teamEnvironment, err := CreateTestEnvironmentInTeam(client, team.Id, rangeChannels, rangeUsers, rangePosts, fuzzy)
if err != true {
- return TestEnviroment{}, false
+ return TestEnvironment{}, false
}
- enviroment.Enviroments[i] = teamEnviroment
+ environment.Environments[i] = teamEnvironment
}
- return enviroment, true
+ return environment, true
}
-func CreateTestEnviromentInTeam(client *model.Client, teamID string, rangeChannels utils.Range, rangeUsers utils.Range, rangePosts utils.Range, fuzzy bool) (TeamEnviroment, bool) {
+func CreateTestEnvironmentInTeam(client *model.Client, teamID string, rangeChannels utils.Range, rangeUsers utils.Range, rangePosts utils.Range, fuzzy bool) (TeamEnvironment, bool) {
rand.Seed(time.Now().UTC().UnixNano())
// We need to create at least one user
@@ -57,7 +57,7 @@ func CreateTestEnviromentInTeam(client *model.Client, teamID string, rangeChanne
userCreator.Fuzzy = fuzzy
users, err := userCreator.CreateTestUsers(rangeUsers)
if err != true {
- return TeamEnviroment{}, false
+ return TeamEnvironment{}, false
}
usernames := make([]string, len(users))
for i, user := range users {
@@ -77,7 +77,7 @@ func CreateTestEnviromentInTeam(client *model.Client, teamID string, rangeChanne
}
if err != true {
- return TeamEnviroment{}, false
+ return TeamEnvironment{}, false
}
numPosts := utils.RandIntFromRange(rangePosts)
numImages := utils.RandIntFromRange(rangePosts) / 4
@@ -93,5 +93,5 @@ func CreateTestEnviromentInTeam(client *model.Client, teamID string, rangeChanne
}
}
- return TeamEnviroment{users, channels}, true
+ return TeamEnvironment{users, channels}, true
}
diff --git a/api/auto_teams.go b/api/auto_teams.go
index 6677ac9bf..082415d32 100644
--- a/api/auto_teams.go
+++ b/api/auto_teams.go
@@ -8,7 +8,7 @@ import (
"github.com/mattermost/platform/utils"
)
-type TeamEnviroment struct {
+type TeamEnvironment struct {
Users []*model.User
Channels []*model.Channel
}
diff --git a/api/command.go b/api/command.go
index b2a4f4a0b..50ca41155 100644
--- a/api/command.go
+++ b/api/command.go
@@ -24,6 +24,7 @@ var (
"loadTestCommand": "/loadtest",
"echoCommand": "/echo",
"shrugCommand": "/shrug",
+ "meCommand": "/me",
}
commands = []commandHandler{
logoutCommand,
@@ -31,6 +32,7 @@ var (
loadTestCommand,
echoCommand,
shrugCommand,
+ meCommand,
}
commandNotImplementedErr = model.NewAppError("checkCommand", "Command not implemented", "")
)
@@ -194,6 +196,34 @@ func echoCommand(c *Context, command *model.Command) bool {
return false
}
+func meCommand(c *Context, command *model.Command) bool {
+ cmd := cmds["meCommand"]
+
+ if !command.Suggest && strings.Index(command.Command, cmd) == 0 {
+ message := ""
+
+ parameters := strings.SplitN(command.Command, " ", 2)
+ if len(parameters) > 1 {
+ message += "*" + parameters[1] + "*"
+ }
+
+ post := &model.Post{}
+ post.Message = message
+ post.ChannelId = command.ChannelId
+ if _, err := CreatePost(c, post, false); err != nil {
+ l4g.Error("Unable to create /me post post, err=%v", err)
+ return false
+ }
+ command.Response = model.RESP_EXECUTED
+ return true
+
+ } else if strings.Index(cmd, command.Command) == 0 {
+ command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: "Do an action, /me [message]"})
+ }
+
+ return false
+}
+
func shrugCommand(c *Context, command *model.Command) bool {
cmd := cmds["shrugCommand"]
@@ -381,11 +411,11 @@ func loadTestSetupCommand(c *Context, command *model.Command) bool {
if doTeams {
if err := CreateBasicUser(client); err != nil {
- l4g.Error("Failed to create testing enviroment")
+ l4g.Error("Failed to create testing environment")
return true
}
client.LoginByEmail(BTEST_TEAM_NAME, BTEST_USER_EMAIL, BTEST_USER_PASSWORD)
- enviroment, err := CreateTestEnviromentWithTeams(
+ environment, err := CreateTestEnvironmentWithTeams(
client,
utils.Range{numTeams, numTeams},
utils.Range{numChannels, numChannels},
@@ -393,18 +423,18 @@ func loadTestSetupCommand(c *Context, command *model.Command) bool {
utils.Range{numPosts, numPosts},
doFuzz)
if err != true {
- l4g.Error("Failed to create testing enviroment")
+ l4g.Error("Failed to create testing environment")
return true
} else {
- l4g.Info("Testing enviroment created")
- for i := 0; i < len(enviroment.Teams); i++ {
- l4g.Info("Team Created: " + enviroment.Teams[i].Name)
- l4g.Info("\t User to login: " + enviroment.Enviroments[i].Users[0].Email + ", " + USER_PASSWORD)
+ l4g.Info("Testing environment created")
+ for i := 0; i < len(environment.Teams); i++ {
+ l4g.Info("Team Created: " + environment.Teams[i].Name)
+ l4g.Info("\t User to login: " + environment.Environments[i].Users[0].Email + ", " + USER_PASSWORD)
}
}
} else {
client.MockSession(c.Session.Token)
- CreateTestEnviromentInTeam(
+ CreateTestEnvironmentInTeam(
client,
c.Session.TeamId,
utils.Range{numChannels, numChannels},
@@ -416,7 +446,7 @@ func loadTestSetupCommand(c *Context, command *model.Command) bool {
} else if strings.Index(cmd, command.Command) == 0 {
command.AddSuggestion(&model.SuggestCommand{
Suggestion: cmd,
- Description: "Creates a testing enviroment in current team. [teams] [fuzz] <Num Channels> <Num Users> <NumPosts>"})
+ Description: "Creates a testing environment in current team. [teams] [fuzz] <Num Channels> <Num Users> <NumPosts>"})
}
return false
diff --git a/config/config.json b/config/config.json
index a927620b5..2738546c0 100644
--- a/config/config.json
+++ b/config/config.json
@@ -92,4 +92,4 @@
"TokenEndpoint": "",
"UserApiEndpoint": ""
}
-} \ No newline at end of file
+}
diff --git a/doc/install/Amazon-Elastic-Beanstalk.md b/doc/install/Amazon-Elastic-Beanstalk.md
index 0416b67ea..8738ab3ac 100644
--- a/doc/install/Amazon-Elastic-Beanstalk.md
+++ b/doc/install/Amazon-Elastic-Beanstalk.md
@@ -1,27 +1,26 @@
## AWS Elastic Beanstalk Setup (Docker)
+These instructions will guide you through the process of setting up Mattermost for product evaluation using an EBS Docker single-container application using [Dockerrun.aws.zip](https://github.com/mattermost/platform/raw/master/docker/1.1/Dockerrun.aws.zip).
-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](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.
- 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.
+1. From your [AWS console]( https://console.aws.amazon.com/console/home) select **Elastic Beanstalk** under the Compute section.
+2. Select **Create New Application** from the top right.
+3. Name your Elastic Beanstalk application and click **Next**,
+4. Select **Create web server** on the New Enviroment page.
+5. If asked, select **Create an IAM role and instance profile**, then click **Next**.
+6. On the Enviroment Type page,
+ 1. Set Predefined Configuration to **Docker** under the generic heading in the drop-down list.
+ 2. Set Environment Type to **Single instance** in the drop-down list.
+ 3. Click **Next**.
+7. For Application Source, select **Upload your own** and upload the [Dockerrun.aws.zip](https://github.com/mattermost/platform/raw/master/docker/1.1/Dockerrun.aws.zip) file, then click **Next**.
+8. Type an Environment Name and URL. Make sure the URL is available by clicking **Check availability**, then click **Next**.
+9. The options on the Additional Resources page may be left at default unless you wish to change them. Click **Next**.
+10. On the Configuration Details page,
+ 1. Select an Instance Type of **t2.small** or larger.
+ 2. The remaining options may be left at their default values unless you wish to change them. Click **Next**.
+11. Environment tags may be left blank. Click **Next**.
+12. You will be asked to review your information, then click **Launch**.
+14. It may take a few minutes for beanstalk to launch your environment. If the launch is successful, you will see a see a large green checkmark and the Health status should change to “Green”.
+15. Test your environment by clicking the domain link next to your application name at the top of the dashboard. Alternatively, enter the domain into your browser in the form `http://<your-ebs-application-url>.elasticbeanstalk.com`. You can also map your own domain if you wish. If everything is working correctly, the domain should navigate you to the Mattermost signup page. Enjoy exploring Mattermost!
- To see the product's full functionality, [enabling SMTP email is recommended](SMTP-Email-Setup.md).
+### (Recommended) Enable Email
+The default single-container Docker instance for Mattermost is designed for product evaluation, and sets `SendEmailNotifications=false` so the product can function without enabling email. To see the product's full functionality, [enabling SMTP email is recommended](SMTP-Email-Setup.md).
diff --git a/doc/install/Requirements.md b/doc/install/Requirements.md
index 1e0a12fb9..61fa8e7be 100644
--- a/doc/install/Requirements.md
+++ b/doc/install/Requirements.md
@@ -10,6 +10,27 @@ Supported Operating Systems and Browsers for the Mattermost Web Client include:
- iPhone 4s and higher (Safari on iOS 8.3+, Chrome 43+)
- Android 5 and higher (Chrome 43+)
+### Email Client
+
+Supported Email Clients for rendering Mattermost email notifications include:
+
+Web based clients:
+- Gmail
+- Office 365
+- Outlook
+- Yahoo
+- AOL
+
+Desktop Clients:
+- Apple Mail version 7+
+- Outlook 2016+
+- Thunderbird 38.2+
+
+Mobile Clients:
+- Gmail Mobile App (Android, iOS)
+- iOS Mail App (iOS 7+)
+- Blackberry Mail App (OS version 4+)
+
### Server
Supported Operating Systems for the Mattermost Server include:
diff --git a/doc/install/SMTP-Email-Setup.md b/doc/install/SMTP-Email-Setup.md
index bb57d95ba..7d9beae89 100644
--- a/doc/install/SMTP-Email-Setup.md
+++ b/doc/install/SMTP-Email-Setup.md
@@ -52,10 +52,18 @@ To enable email, configure an SMTP email service as follows:
* Set **Connection Security** to **(empty)**
##### Gmail
-* Information needed
+* Set **SMTP Username** to **your_email@gmail.com**
+* Set **SMTP Password** to **your_password**
+* Set **SMTP Server** to **smtp.gmail.com**
+* Set **SMTP Port** to **587**
+* Set **Connection Security** to **TLS**
##### Office 365
-* Information needed
+* Set **SMTP Username** to **Office 365 username**
+* Set **SMTP Password** to **Office 365 password**
+* Set **SMTP Server** to **smtp.office365.com**
+* Set **SMTP Port** to **587**
+* Set **Connection Security** to **TLS**
##### Hotmail
* Set **SMTP Username** to **your_email@hotmail.com**
diff --git a/doc/integrations/webhooks/Incoming-Webhooks.md b/doc/integrations/webhooks/Incoming-Webhooks.md
index b10b6e342..b5ae0fde2 100644
--- a/doc/integrations/webhooks/Incoming-Webhooks.md
+++ b/doc/integrations/webhooks/Incoming-Webhooks.md
@@ -90,9 +90,9 @@ As mentioned above, Mattermost makes it easy to take integrations written for Sl
To see samples and community contributions, please visit <http://mattermost.org/webhooks>.
-#### Known Issues
+#### Known Issues in v1.1
- The `attachments` payload used in Slack is not yet supported
-- Overriding of usernames does not yet apply to notifications
+- Overriding of usernames does not yet apply to notifications (fixed on master)
- Cannot supply `icon_emoji` to override the message icon
-- Webhook UI fails when connected to deleted channel
+- Webhook UI fails when connected to deleted channel (fixed on master)
diff --git a/doc/integrations/webhooks/Outgoing-Webhooks.md b/doc/integrations/webhooks/Outgoing-Webhooks.md
index abe26ceae..008245715 100644
--- a/doc/integrations/webhooks/Outgoing-Webhooks.md
+++ b/doc/integrations/webhooks/Outgoing-Webhooks.md
@@ -114,7 +114,7 @@ As mentioned above, Mattermost makes it easy to take integrations written for Sl
To see samples and community contributions, please visit <http://mattermost.org/webhooks>.
-#### Known Issues
+#### Known Issues in v1.1
- Overriding of usernames does not yet apply to notifications
- Cannot supply `icon_emoji` to override the message icon
diff --git a/doc/process/documentation-guidelines.md b/doc/process/documentation-guidelines.md
new file mode 100644
index 000000000..f37f0c5fc
--- /dev/null
+++ b/doc/process/documentation-guidelines.md
@@ -0,0 +1,185 @@
+# Documentation Conventions
+
+The most important thing about documentation is getting it done and out to the community.
+
+After that, we can work on upgrading the quality of documentation. The below chart summarizes the different levels of documentation and how the quality gates are applied.
+
+_Note: Documentation Guidelines are new, and iterating. Documentation has started to balloon and this is our attempt at reducing ambiguity and increasing consistency, but the conventions here are very open to discussion._
+
+| Stars | Benchmark | Timeline |
+|:-------------|:--------------------------------|:--------------------------------|
+| 1 | Documentation is correct. | First draft checked in by developer. Okay to ship in first release of new feature. |
+| 2 | Documentation a) follows all objective formatting criteria, b) is tested by someone other than the author, c) satisfies above. | First edit under objective rules. Required before second release cycle with this feature included. |
+| 3 | Documentation a) follows all subjective style criteria, b) is reviewed and edited by someone who has previously authored 3-star documentation, and c) satisfies above. | Second edit under subjective rules. Required before third release cycle with this feature included |
+| 4 | Documentation a) has received at least 1 edit due to user feedback, b) has received at least one unprompted compliment from user community on quality, c) satisfies above. | Additional edits to refine documentation based on user feedback |
+
+## 1-Star Requirements: Correctness
+
+### List precise dependencies
+
+1. Be explicit about what specific dependencies have been tested as part of an installation procedure.
+2. Be explicit about assumptions of compatibility on systems that have not been tested.
+3. Do not claim the system works on later versions of a platform if backwards compatibility is not a priority for the dependency (It's okay to say Chrome version 43 and higher, but not Python 2.6 and higher, because Python 3.0 is explicitly incompatible with previous versions).
+
+#### Correct
+
+----
+This procedure works on an Ubuntu 14.04 server with Python 2.6 installed and should work on compatible Linux-based operating systems and compatible versions of Python.
+
+----
+#### Incorrect
+
+----
+This procedure works on Linux servers running Python.
+
+also:
+
+This procedure works on Linux servers running Python 2.6 and higher.
+
+----
+## 2-Star Requirements: Objective Formatting Checklist
+
+### Use headings
+
+Headings in markdown provide anchors that can be used to easily reference sub-sections of long pieces of documentation. This is preferrable to just numbering sections without headings.
+
+##### Correct:
+
+----
+##### Step 1: Add a heading
+This makes things easier to reference via hyperlinks
+##### Step 2: Link to headings
+So things are eariser to find
+
+----
+##### Incorrect:
+
+----
+**Step 1: Add a heading**
+This makes things easier to reference via hyperlinks
+**Step 2: Link to headings**
+So things are eariser to find
+----
+
+### Use appropriate heading case
+
+Cases in headings may vary depending on usage.
+
+#### When to use Title Case
+
+H1, H2, H3 headings should be "Title Case" and less than four words, except if a colon is used, then four words per segment separated by the colon.
+
+These large headings are typically shorter and help with navigating large documents
+
+#### When to use sentence case
+
+H3, H4, H5 headings should be "Sentence case" and can be any length.
+
+These headers are smaller and used to summarize sections. H3 can be considered either a large or small heading.
+
+These conventions are new, so there's flexibility around them, when you're not sure, consider the convention here as default.
+
+### One instruction per line
+
+It's easy to miss instructions when they're compounded. Have only one instruction per line, so documentation looks more like a checklist.
+
+A support person should be able to say "Did you complete step 7?" instead of "Did you complete the second part of step 7 after doing XXX?"
+
+##### Correct:
+
+----
+
+6. For **Predefined configuration** look under **Generic** and select **Docker**.
+ 7. For **Environment type** select **Single instance**
+
+----
+
+##### Incorrect:
+
+----
+
+6. For **Predefined configuration** look under **Generic** and select **Docker**. For **Environment type** select **Single instance**
+
+----
+
+### Lists end without periods
+
+Sentences within bullet points or numbered lists should end in normal punctuation. The sentence or fragment at the end of a bullet point should not have a period.
+
+##### Correct
+
+----
+- This is a sentence within a bullet point. This is the end of a bullet point without a period
+
+----
+##### Incorrect
+
+----
+- This is an incorrect ending of a bullet point with a period.
+
+----
+### Avoid Passive Phrases
+
+Examples of passive phrases include "have", "had", "was", "can be", "has been" and documentation is shorter and clearer without them.
+
+##### Correct
+
+----
+This software **runs** on any server that supports Python.
+
+----
+##### Incorrect
+
+----
+This software **can be run** on any server that supports Python.
+
+----
+## 3-Star Requirements: Subjective Style Guidelines
+
+### Be Concise
+
+Try to use fewer words when possible.
+
+##### Correct:
+
+----
+This integration posts [issue](http://doc.gitlab.com/ee/web_hooks/web_hooks.html#issues-events), [comment](http://doc.gitlab.com/ee/web_hooks/web_hooks.html#comment-events) and [merge request](http://doc.gitlab.com/ee/web_hooks/web_hooks.html#merge-request-events) events from a GitLab repository into specific Mattermost channels by formatting output from [GitLab's outgoing webhooks](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/web_hooks/web_hooks.md) to [Mattermost's incoming webhooks](https://github.com/mattermost/platform/blob/master/doc/integrations/webhooks/Incoming-Webhooks.md).
+
+----
+##### Incorrect:
+
+----
+This integration makes use of GitLab's outgoing webhooks and Mattermost's incoming webhooks to post GitLab events into Mattermost. You can find GitLab's outgoing webhooks described [here](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/web_hooks/web_hooks.md) and Mattermost's incoming webhooks described [here](https://github.com/mattermost/platform/blob/master/doc/integrations/webhooks/Incoming-Webhooks.md).
+
+----
+
+### Use appropriate emphasis
+
+Mention Clickable Controls in **Bold**, Sections and Setting Names in *Italics*, and Key Strokes in `pre-formatted text`.
+
+To make it clear and consistent across documentation on how we describe controls that a user is asked to manipulate, we have a number of guidelines:
+
+**Bold**
+- Please **bold** the names of controls you're asking users to click. The text that is bolded should match the label of the control in the user interface. Do not format these references with _italics_, ALL-CAPS or `pre-formatted text`.
+- Use `>` to express a series of clicks, for example clicking on **Button One** > **Button Two** > **Button Three**.
+- If a button might be difficult to find, give a hint about its location _before_ mentioning the name of the control (this helps people find the hint before they start searching, if the see the name of the button first, they might not continue reading to find the hint before starting to look).
+
+***Italics***
+- Please *italicize* setting names or section headings that identify that the user is looking in the correct area. The text that is italicized should match the name of the setting or section in the user interface.
+- It is helpful to use italics to guide the user to the correct area before mentioning a clickable action in bold.
+
+**`pre-formatted text`**
+- Please use `pre-formatted text` to identify when a user must enter key strokes or paste text into an input box.
+
+#### Correct
+
+----
+Type `mattermost-integration-giphy` in the *repo-name* field, then click **Search** and then the **Connect** button once Heroku finds your repository
+
+----
+#### Incorrect
+
+----
+Type "mattermost-integration-giphy" in the **repo-name** field, then click Search and then the *Connect* button once Heroku finds your repository
+
+----
diff --git a/manualtesting/manual_testing.go b/manualtesting/manual_testing.go
index 3c2289626..ffdb578a4 100644
--- a/manualtesting/manual_testing.go
+++ b/manualtesting/manual_testing.go
@@ -16,7 +16,7 @@ import (
"time"
)
-type TestEnviroment struct {
+type TestEnvironment struct {
Params map[string][]string
Client *model.Client
CreatedTeamId string
@@ -121,8 +121,8 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/channels/town-square", http.StatusTemporaryRedirect)
}
- // Setup test enviroment
- env := TestEnviroment{
+ // Setup test environment
+ env := TestEnvironment{
Params: params,
Client: client,
CreatedTeamId: teamID,
diff --git a/manualtesting/test_autolink.go b/manualtesting/test_autolink.go
index e4d49659a..a13ec7a75 100644
--- a/manualtesting/test_autolink.go
+++ b/manualtesting/test_autolink.go
@@ -19,7 +19,7 @@ http://www.google.com.pk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=4&cad=rja&uact=8
https://medium.com/@slackhq/11-useful-tips-for-getting-the-most-of-slack-5dfb3d1af77
`
-func testAutoLink(env TestEnviroment) *model.AppError {
+func testAutoLink(env TestEnvironment) *model.AppError {
l4g.Info("Manual Auto Link Test")
channelID, err := getChannelID(model.DEFAULT_CHANNEL, env.CreatedTeamId, env.CreatedUserId)
if err != true {
diff --git a/model/outgoing_webhook.go b/model/outgoing_webhook.go
index 8958dd5b0..9a1b89a85 100644
--- a/model/outgoing_webhook.go
+++ b/model/outgoing_webhook.go
@@ -100,6 +100,12 @@ func (o *OutgoingWebhook) IsValid() *AppError {
return NewAppError("OutgoingWebhook.IsValid", "Invalid callback urls", "")
}
+ for _, callback := range o.CallbackURLs {
+ if !IsValidHttpUrl(callback) {
+ return NewAppError("OutgoingWebhook.IsValid", "Invalid callback URLs. Each must be a valid URL and start with http:// or https://", "")
+ }
+ }
+
return nil
}
diff --git a/model/outgoing_webhook_test.go b/model/outgoing_webhook_test.go
index 2ca48c291..0d1cd773e 100644
--- a/model/outgoing_webhook_test.go
+++ b/model/outgoing_webhook_test.go
@@ -80,6 +80,11 @@ func TestOutgoingWebhookIsValid(t *testing.T) {
t.Fatal("should be invalid")
}
+ o.CallbackURLs = []string{"nowhere.com/"}
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
o.CallbackURLs = []string{"http://nowhere.com/"}
if err := o.IsValid(); err != nil {
t.Fatal(err)
diff --git a/model/team.go b/model/team.go
index 4d14ec2ee..5c9cf5a26 100644
--- a/model/team.go
+++ b/model/team.go
@@ -229,6 +229,5 @@ func (o *Team) PreExport() {
func (o *Team) Sanitize() {
o.Email = ""
- o.Type = ""
o.AllowedDomains = ""
}
diff --git a/model/utils.go b/model/utils.go
index bb0669df7..681ade870 100644
--- a/model/utils.go
+++ b/model/utils.go
@@ -11,6 +11,7 @@ import (
"fmt"
"io"
"net/mail"
+ "net/url"
"regexp"
"strings"
"time"
@@ -301,3 +302,15 @@ var UrlRegex = regexp.MustCompile(`^((?:[a-z]+:\/\/)?(?:(?:[a-z0-9\-]+\.)+(?:[a-
var PartialUrlRegex = regexp.MustCompile(`/([A-Za-z0-9]{26})/([A-Za-z0-9]{26})/((?:[A-Za-z0-9]{26})?.+(?:\.[A-Za-z0-9]{3,})?)`)
var SplitRunes = map[rune]bool{',': true, ' ': true, '.': true, '!': true, '?': true, ':': true, ';': true, '\n': true, '<': true, '>': true, '(': true, ')': true, '{': true, '}': true, '[': true, ']': true, '+': true, '/': true, '\\': true}
+
+func IsValidHttpUrl(rawUrl string) bool {
+ if strings.Index(rawUrl, "http://") != 0 && strings.Index(rawUrl, "https://") != 0 {
+ return false
+ }
+
+ if _, err := url.ParseRequestURI(rawUrl); err != nil {
+ return false
+ }
+
+ return true
+}
diff --git a/store/sql_audit_store.go b/store/sql_audit_store.go
index 898cf8f78..b3e2daea0 100644
--- a/store/sql_audit_store.go
+++ b/store/sql_audit_store.go
@@ -46,7 +46,7 @@ func (s SqlAuditStore) Save(audit *model.Audit) StoreChannel {
if err := s.GetMaster().Insert(audit); err != nil {
result.Err = model.NewAppError("SqlAuditStore.Save",
- "We encounted an error saving the audit", "user_id="+
+ "We encountered an error saving the audit", "user_id="+
audit.UserId+" action="+audit.Action)
}
@@ -75,7 +75,7 @@ func (s SqlAuditStore) Get(user_id string, limit int) StoreChannel {
var audits model.Audits
if _, err := s.GetReplica().Select(&audits, "SELECT * FROM Audits WHERE UserId = :user_id ORDER BY CreateAt DESC LIMIT :limit",
map[string]interface{}{"user_id": user_id, "limit": limit}); err != nil {
- result.Err = model.NewAppError("SqlAuditStore.Get", "We encounted an error finding the audits", "user_id="+user_id)
+ result.Err = model.NewAppError("SqlAuditStore.Get", "We encountered an error finding the audits", "user_id="+user_id)
} else {
result.Data = audits
}
diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go
index 14896c0a0..2d62b8614 100644
--- a/store/sql_channel_store.go
+++ b/store/sql_channel_store.go
@@ -247,7 +247,7 @@ func (s SqlChannelStore) Update(channel *model.Channel) StoreChannel {
result.Err = model.NewAppError("SqlChannelStore.Update", "A channel with that handle already exists", "id="+channel.Id+", "+err.Error())
}
} else {
- result.Err = model.NewAppError("SqlChannelStore.Update", "We encounted an error updating the channel", "id="+channel.Id+", "+err.Error())
+ result.Err = model.NewAppError("SqlChannelStore.Update", "We encountered an error updating the channel", "id="+channel.Id+", "+err.Error())
}
} else if count != 1 {
result.Err = model.NewAppError("SqlChannelStore.Update", "We couldn't update the channel", "id="+channel.Id)
@@ -297,7 +297,7 @@ func (s SqlChannelStore) Get(id string) StoreChannel {
result := StoreResult{}
if obj, err := s.GetReplica().Get(model.Channel{}, id); err != nil {
- result.Err = model.NewAppError("SqlChannelStore.Get", "We encounted an error finding the channel", "id="+id+", "+err.Error())
+ result.Err = model.NewAppError("SqlChannelStore.Get", "We encountered an error finding the channel", "id="+id+", "+err.Error())
} else if obj == nil {
result.Err = model.NewAppError("SqlChannelStore.Get", "We couldn't find the existing channel", "id="+id)
} else {
@@ -537,7 +537,7 @@ func (s SqlChannelStore) UpdateMember(member *model.ChannelMember) StoreChannel
}
if _, err := s.GetMaster().Update(member); err != nil {
- result.Err = model.NewAppError("SqlChannelStore.UpdateMember", "We encounted an error updating the channel member",
+ result.Err = model.NewAppError("SqlChannelStore.UpdateMember", "We encountered an error updating the channel member",
"channel_id="+member.ChannelId+", "+"user_id="+member.UserId+", "+err.Error())
} else {
result.Data = member
diff --git a/store/sql_oauth_store.go b/store/sql_oauth_store.go
index db52e379b..751207b85 100644
--- a/store/sql_oauth_store.go
+++ b/store/sql_oauth_store.go
@@ -102,7 +102,7 @@ func (as SqlOAuthStore) UpdateApp(app *model.OAuthApp) StoreChannel {
}
if oldAppResult, err := as.GetMaster().Get(model.OAuthApp{}, app.Id); err != nil {
- result.Err = model.NewAppError("SqlOAuthStore.UpdateApp", "We encounted an error finding the app", "app_id="+app.Id+", "+err.Error())
+ result.Err = model.NewAppError("SqlOAuthStore.UpdateApp", "We encountered an error finding the app", "app_id="+app.Id+", "+err.Error())
} else if oldAppResult == nil {
result.Err = model.NewAppError("SqlOAuthStore.UpdateApp", "We couldn't find the existing app to update", "app_id="+app.Id)
} else {
@@ -112,7 +112,7 @@ func (as SqlOAuthStore) UpdateApp(app *model.OAuthApp) StoreChannel {
app.CreatorId = oldApp.CreatorId
if count, err := as.GetMaster().Update(app); err != nil {
- result.Err = model.NewAppError("SqlOAuthStore.UpdateApp", "We encounted an error updating the app", "app_id="+app.Id+", "+err.Error())
+ result.Err = model.NewAppError("SqlOAuthStore.UpdateApp", "We encountered an error updating the app", "app_id="+app.Id+", "+err.Error())
} else if count != 1 {
result.Err = model.NewAppError("SqlOAuthStore.UpdateApp", "We couldn't update the app", "app_id="+app.Id)
} else {
@@ -135,7 +135,7 @@ func (as SqlOAuthStore) GetApp(id string) StoreChannel {
result := StoreResult{}
if obj, err := as.GetReplica().Get(model.OAuthApp{}, id); err != nil {
- result.Err = model.NewAppError("SqlOAuthStore.GetApp", "We encounted an error finding the app", "app_id="+id+", "+err.Error())
+ result.Err = model.NewAppError("SqlOAuthStore.GetApp", "We encountered an error finding the app", "app_id="+id+", "+err.Error())
} else if obj == nil {
result.Err = model.NewAppError("SqlOAuthStore.GetApp", "We couldn't find the existing app", "app_id="+id)
} else {
@@ -208,7 +208,7 @@ func (as SqlOAuthStore) GetAccessData(token string) StoreChannel {
accessData := model.AccessData{}
if err := as.GetReplica().SelectOne(&accessData, "SELECT * FROM OAuthAccessData WHERE Token = :Token", map[string]interface{}{"Token": token}); err != nil {
- result.Err = model.NewAppError("SqlOAuthStore.GetAccessData", "We encounted an error finding the access token", err.Error())
+ result.Err = model.NewAppError("SqlOAuthStore.GetAccessData", "We encountered an error finding the access token", err.Error())
} else {
result.Data = &accessData
}
@@ -300,7 +300,7 @@ func (as SqlOAuthStore) GetAuthData(code string) StoreChannel {
result := StoreResult{}
if obj, err := as.GetReplica().Get(model.AuthData{}, code); err != nil {
- result.Err = model.NewAppError("SqlOAuthStore.GetAuthData", "We encounted an error finding the authorization code", err.Error())
+ result.Err = model.NewAppError("SqlOAuthStore.GetAuthData", "We encountered an error finding the authorization code", err.Error())
} else if obj == nil {
result.Err = model.NewAppError("SqlOAuthStore.GetAuthData", "We couldn't find the existing authorization code", "")
} else {
diff --git a/store/sql_post_store.go b/store/sql_post_store.go
index 7894ff488..de8c4f356 100644
--- a/store/sql_post_store.go
+++ b/store/sql_post_store.go
@@ -443,13 +443,6 @@ func (s SqlPostStore) Search(teamId string, userId string, params *model.SearchP
var posts []*model.Post
- if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
- // Parse text for wildcards
- if wildcard, err := regexp.Compile("\\*($| )"); err == nil {
- terms = wildcard.ReplaceAllLiteralString(terms, "* ")
- }
- }
-
searchQuery := `
SELECT
*
@@ -548,7 +541,7 @@ func (s SqlPostStore) Search(teamId string, userId string, params *model.SearchP
_, err := s.GetReplica().Select(&posts, searchQuery, queryParams)
if err != nil {
- result.Err = model.NewAppError("SqlPostStore.Search", "We encounted an error while searching for posts", "teamId="+teamId+", err="+err.Error())
+ result.Err = model.NewAppError("SqlPostStore.Search", "We encountered an error while searching for posts", "teamId="+teamId+", err="+err.Error())
}
list := &model.PostList{Order: make([]string, 0, len(posts))}
diff --git a/store/sql_post_store_test.go b/store/sql_post_store_test.go
index 872423c5a..0980b1a11 100644
--- a/store/sql_post_store_test.go
+++ b/store/sql_post_store_test.go
@@ -526,32 +526,32 @@ func TestPostStoreSearch(t *testing.T) {
o5 = (<-store.Post().Save(o5)).Data.(*model.Post)
r1 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "corey", IsHashtag: false})).Data.(*model.PostList)
- if len(r1.Order) != 1 && r1.Order[0] != o1.Id {
+ if len(r1.Order) != 1 || r1.Order[0] != o1.Id {
t.Fatal("returned wrong search result")
}
r3 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "new", IsHashtag: false})).Data.(*model.PostList)
- if len(r3.Order) != 2 && r3.Order[0] != o1.Id {
+ if len(r3.Order) != 2 || (r3.Order[0] != o1.Id && r3.Order[1] != o1.Id) {
t.Fatal("returned wrong search result")
}
r4 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "john", IsHashtag: false})).Data.(*model.PostList)
- if len(r4.Order) != 1 && r4.Order[0] != o2.Id {
+ if len(r4.Order) != 1 || r4.Order[0] != o2.Id {
t.Fatal("returned wrong search result")
}
r5 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "matter*", IsHashtag: false})).Data.(*model.PostList)
- if len(r5.Order) != 1 && r5.Order[0] != o1.Id {
+ if len(r5.Order) != 1 || r5.Order[0] != o1.Id {
t.Fatal("returned wrong search result")
}
r6 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "#hashtag", IsHashtag: true})).Data.(*model.PostList)
- if len(r6.Order) != 1 && r6.Order[0] != o4.Id {
+ if len(r6.Order) != 1 || r6.Order[0] != o4.Id {
t.Fatal("returned wrong search result")
}
r7 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "#secret", IsHashtag: true})).Data.(*model.PostList)
- if len(r7.Order) != 1 && r7.Order[0] != o5.Id {
+ if len(r7.Order) != 1 || r7.Order[0] != o5.Id {
t.Fatal("returned wrong search result")
}
diff --git a/store/sql_preference_store.go b/store/sql_preference_store.go
index bf6e030bf..f9f38b747 100644
--- a/store/sql_preference_store.go
+++ b/store/sql_preference_store.go
@@ -173,7 +173,7 @@ func (s SqlPreferenceStore) Get(userId string, category string, name string) Sto
UserId = :UserId
AND Category = :Category
AND Name = :Name`, map[string]interface{}{"UserId": userId, "Category": category, "Name": name}); err != nil {
- result.Err = model.NewAppError("SqlPreferenceStore.Get", "We encounted an error while finding preferences", err.Error())
+ result.Err = model.NewAppError("SqlPreferenceStore.Get", "We encountered an error while finding preferences", err.Error())
} else {
result.Data = preference
}
@@ -201,7 +201,7 @@ func (s SqlPreferenceStore) GetCategory(userId string, category string) StoreCha
WHERE
UserId = :UserId
AND Category = :Category`, map[string]interface{}{"UserId": userId, "Category": category}); err != nil {
- result.Err = model.NewAppError("SqlPreferenceStore.GetCategory", "We encounted an error while finding preferences", err.Error())
+ result.Err = model.NewAppError("SqlPreferenceStore.GetCategory", "We encountered an error while finding preferences", err.Error())
} else {
result.Data = preferences
}
@@ -228,7 +228,7 @@ func (s SqlPreferenceStore) GetAll(userId string) StoreChannel {
Preferences
WHERE
UserId = :UserId`, map[string]interface{}{"UserId": userId}); err != nil {
- result.Err = model.NewAppError("SqlPreferenceStore.GetAll", "We encounted an error while finding preferences", err.Error())
+ result.Err = model.NewAppError("SqlPreferenceStore.GetAll", "We encountered an error while finding preferences", err.Error())
} else {
result.Data = preferences
}
diff --git a/store/sql_session_store.go b/store/sql_session_store.go
index 15ec6924b..27b34ee39 100644
--- a/store/sql_session_store.go
+++ b/store/sql_session_store.go
@@ -80,9 +80,9 @@ func (me SqlSessionStore) Get(sessionIdOrToken string) StoreChannel {
var sessions []*model.Session
if _, err := me.GetReplica().Select(&sessions, "SELECT * FROM Sessions WHERE Token = :Token OR Id = :Id LIMIT 1", map[string]interface{}{"Token": sessionIdOrToken, "Id": sessionIdOrToken}); err != nil {
- result.Err = model.NewAppError("SqlSessionStore.Get", "We encounted an error finding the session", "sessionIdOrToken="+sessionIdOrToken+", "+err.Error())
+ result.Err = model.NewAppError("SqlSessionStore.Get", "We encountered an error finding the session", "sessionIdOrToken="+sessionIdOrToken+", "+err.Error())
} else if sessions == nil || len(sessions) == 0 {
- result.Err = model.NewAppError("SqlSessionStore.Get", "We encounted an error finding the session", "sessionIdOrToken="+sessionIdOrToken)
+ result.Err = model.NewAppError("SqlSessionStore.Get", "We encountered an error finding the session", "sessionIdOrToken="+sessionIdOrToken)
} else {
result.Data = sessions[0]
}
@@ -109,7 +109,7 @@ func (me SqlSessionStore) GetSessions(userId string) StoreChannel {
var sessions []*model.Session
if _, err := me.GetReplica().Select(&sessions, "SELECT * FROM Sessions WHERE UserId = :UserId ORDER BY LastActivityAt DESC", map[string]interface{}{"UserId": userId}); err != nil {
- result.Err = model.NewAppError("SqlSessionStore.GetSessions", "We encounted an error while finding user sessions", err.Error())
+ result.Err = model.NewAppError("SqlSessionStore.GetSessions", "We encountered an error while finding user sessions", err.Error())
} else {
result.Data = sessions
@@ -165,7 +165,7 @@ func (me SqlSessionStore) CleanUpExpiredSessions(userId string) StoreChannel {
result := StoreResult{}
if _, err := me.GetMaster().Exec("DELETE FROM Sessions WHERE UserId = :UserId AND ExpiresAt != 0 AND :ExpiresAt > ExpiresAt", map[string]interface{}{"UserId": userId, "ExpiresAt": model.GetMillis()}); err != nil {
- result.Err = model.NewAppError("SqlSessionStore.CleanUpExpiredSessions", "We encounted an error while deleting expired user sessions", err.Error())
+ result.Err = model.NewAppError("SqlSessionStore.CleanUpExpiredSessions", "We encountered an error while deleting expired user sessions", err.Error())
} else {
result.Data = userId
}
diff --git a/store/sql_store.go b/store/sql_store.go
index 8965fef64..e5c540e06 100644
--- a/store/sql_store.go
+++ b/store/sql_store.go
@@ -121,7 +121,10 @@ func NewSqlStore() Store {
sqlStore.webhook = NewSqlWebhookStore(sqlStore)
sqlStore.preference = NewSqlPreferenceStore(sqlStore)
- sqlStore.master.CreateTablesIfNotExists()
+ err := sqlStore.master.CreateTablesIfNotExists()
+ if err != nil {
+ l4g.Critical("Error creating database tables: %v", err)
+ }
sqlStore.team.(*SqlTeamStore).UpgradeSchemaIfNeeded()
sqlStore.channel.(*SqlChannelStore).UpgradeSchemaIfNeeded()
diff --git a/store/sql_system_store.go b/store/sql_system_store.go
index a4cb52d4d..1fbdfb333 100644
--- a/store/sql_system_store.go
+++ b/store/sql_system_store.go
@@ -37,7 +37,7 @@ func (s SqlSystemStore) Save(system *model.System) StoreChannel {
result := StoreResult{}
if err := s.GetMaster().Insert(system); err != nil {
- result.Err = model.NewAppError("SqlSystemStore.Save", "We encounted an error saving the system property", "")
+ result.Err = model.NewAppError("SqlSystemStore.Save", "We encountered an error saving the system property", "")
}
storeChannel <- result
@@ -55,7 +55,7 @@ func (s SqlSystemStore) Update(system *model.System) StoreChannel {
result := StoreResult{}
if _, err := s.GetMaster().Update(system); err != nil {
- result.Err = model.NewAppError("SqlSystemStore.Save", "We encounted an error updating the system property", "")
+ result.Err = model.NewAppError("SqlSystemStore.Save", "We encountered an error updating the system property", "")
}
storeChannel <- result
@@ -75,7 +75,7 @@ func (s SqlSystemStore) Get() StoreChannel {
var systems []model.System
props := make(model.StringMap)
if _, err := s.GetReplica().Select(&systems, "SELECT * FROM Systems"); err != nil {
- result.Err = model.NewAppError("SqlSystemStore.Get", "We encounted an error finding the system properties", "")
+ result.Err = model.NewAppError("SqlSystemStore.Get", "We encountered an error finding the system properties", "")
} else {
for _, prop := range systems {
props[prop.Name] = prop.Value
diff --git a/store/sql_team_store.go b/store/sql_team_store.go
index ebf982ec4..dfc07d3d8 100644
--- a/store/sql_team_store.go
+++ b/store/sql_team_store.go
@@ -97,7 +97,7 @@ func (s SqlTeamStore) Update(team *model.Team) StoreChannel {
}
if oldResult, err := s.GetMaster().Get(model.Team{}, team.Id); err != nil {
- result.Err = model.NewAppError("SqlTeamStore.Update", "We encounted an error finding the team", "id="+team.Id+", "+err.Error())
+ result.Err = model.NewAppError("SqlTeamStore.Update", "We encountered an error finding the team", "id="+team.Id+", "+err.Error())
} else if oldResult == nil {
result.Err = model.NewAppError("SqlTeamStore.Update", "We couldn't find the existing team to update", "id="+team.Id)
} else {
@@ -107,7 +107,7 @@ func (s SqlTeamStore) Update(team *model.Team) StoreChannel {
team.Name = oldTeam.Name
if count, err := s.GetMaster().Update(team); err != nil {
- result.Err = model.NewAppError("SqlTeamStore.Update", "We encounted an error updating the team", "id="+team.Id+", "+err.Error())
+ result.Err = model.NewAppError("SqlTeamStore.Update", "We encountered an error updating the team", "id="+team.Id+", "+err.Error())
} else if count != 1 {
result.Err = model.NewAppError("SqlTeamStore.Update", "We couldn't update the team", "id="+team.Id)
} else {
@@ -149,7 +149,7 @@ func (s SqlTeamStore) Get(id string) StoreChannel {
result := StoreResult{}
if obj, err := s.GetReplica().Get(model.Team{}, id); err != nil {
- result.Err = model.NewAppError("SqlTeamStore.Get", "We encounted an error finding the team", "id="+id+", "+err.Error())
+ result.Err = model.NewAppError("SqlTeamStore.Get", "We encountered an error finding the team", "id="+id+", "+err.Error())
} else if obj == nil {
result.Err = model.NewAppError("SqlTeamStore.Get", "We couldn't find the existing team", "id="+id)
} else {
@@ -230,7 +230,7 @@ func (s SqlTeamStore) GetTeamsForEmail(email string) StoreChannel {
var data []*model.Team
if _, err := s.GetReplica().Select(&data, "SELECT Teams.* FROM Teams, Users WHERE Teams.Id = Users.TeamId AND Users.Email = :Email", map[string]interface{}{"Email": email}); err != nil {
- result.Err = model.NewAppError("SqlTeamStore.GetTeamsForEmail", "We encounted a problem when looking up teams", "email="+email+", "+err.Error())
+ result.Err = model.NewAppError("SqlTeamStore.GetTeamsForEmail", "We encountered a problem when looking up teams", "email="+email+", "+err.Error())
}
for _, team := range data {
diff --git a/store/sql_user_store.go b/store/sql_user_store.go
index d825cda57..3347df08b 100644
--- a/store/sql_user_store.go
+++ b/store/sql_user_store.go
@@ -118,7 +118,7 @@ func (us SqlUserStore) Update(user *model.User, allowActiveUpdate bool) StoreCha
}
if oldUserResult, err := us.GetMaster().Get(model.User{}, user.Id); err != nil {
- result.Err = model.NewAppError("SqlUserStore.Update", "We encounted an error finding the account", "user_id="+user.Id+", "+err.Error())
+ result.Err = model.NewAppError("SqlUserStore.Update", "We encountered an error finding the account", "user_id="+user.Id+", "+err.Error())
} else if oldUserResult == nil {
result.Err = model.NewAppError("SqlUserStore.Update", "We couldn't find the existing account to update", "user_id="+user.Id)
} else {
@@ -161,7 +161,7 @@ func (us SqlUserStore) Update(user *model.User, allowActiveUpdate bool) StoreCha
} else if IsUniqueConstraintError(err.Error(), "Username", "users_username_teamid_key") {
result.Err = model.NewAppError("SqlUserStore.Update", "This username is already taken. Please choose another.", "user_id="+user.Id+", "+err.Error())
} else {
- result.Err = model.NewAppError("SqlUserStore.Update", "We encounted an error updating the account", "user_id="+user.Id+", "+err.Error())
+ result.Err = model.NewAppError("SqlUserStore.Update", "We encountered an error updating the account", "user_id="+user.Id+", "+err.Error())
}
} else if count != 1 {
result.Err = model.NewAppError("SqlUserStore.Update", "We couldn't update the account", fmt.Sprintf("user_id=%v, count=%v", user.Id, count))
@@ -306,7 +306,7 @@ func (us SqlUserStore) Get(id string) StoreChannel {
result := StoreResult{}
if obj, err := us.GetReplica().Get(model.User{}, id); err != nil {
- result.Err = model.NewAppError("SqlUserStore.Get", "We encounted an error finding the account", "user_id="+id+", "+err.Error())
+ result.Err = model.NewAppError("SqlUserStore.Get", "We encountered an error finding the account", "user_id="+id+", "+err.Error())
} else if obj == nil {
result.Err = model.NewAppError("SqlUserStore.Get", "We couldn't find the existing account", "user_id="+id)
} else {
@@ -351,7 +351,7 @@ func (us SqlUserStore) GetProfiles(teamId string) StoreChannel {
var users []*model.User
if _, err := us.GetReplica().Select(&users, "SELECT * FROM Users WHERE TeamId = :TeamId", map[string]interface{}{"TeamId": teamId}); err != nil {
- result.Err = model.NewAppError("SqlUserStore.GetProfiles", "We encounted an error while finding user profiles", err.Error())
+ result.Err = model.NewAppError("SqlUserStore.GetProfiles", "We encountered an error while finding user profiles", err.Error())
} else {
userMap := make(map[string]*model.User)
@@ -382,7 +382,7 @@ func (us SqlUserStore) GetSystemAdminProfiles() StoreChannel {
var users []*model.User
if _, err := us.GetReplica().Select(&users, "SELECT * FROM Users WHERE Roles = :Roles", map[string]interface{}{"Roles": "system_admin"}); err != nil {
- result.Err = model.NewAppError("SqlUserStore.GetSystemAdminProfiles", "We encounted an error while finding user profiles", err.Error())
+ result.Err = model.NewAppError("SqlUserStore.GetSystemAdminProfiles", "We encountered an error while finding user profiles", err.Error())
} else {
userMap := make(map[string]*model.User)
@@ -498,7 +498,7 @@ func (us SqlUserStore) GetForExport(teamId string) StoreChannel {
var users []*model.User
if _, err := us.GetReplica().Select(&users, "SELECT * FROM Users WHERE TeamId = :TeamId", map[string]interface{}{"TeamId": teamId}); err != nil {
- result.Err = model.NewAppError("SqlUserStore.GetProfiles", "We encounted an error while finding user profiles", err.Error())
+ result.Err = model.NewAppError("SqlUserStore.GetProfiles", "We encountered an error while finding user profiles", err.Error())
} else {
for _, u := range users {
u.Password = ""
diff --git a/web/react/components/admin_console/gitlab_settings.jsx b/web/react/components/admin_console/gitlab_settings.jsx
index 8b0f00083..f8fb6d115 100644
--- a/web/react/components/admin_console/gitlab_settings.jsx
+++ b/web/react/components/admin_console/gitlab_settings.jsx
@@ -259,7 +259,6 @@ export default class GitLabSettings extends React.Component {
}
}
-
//config.GitLabSettings.Scope = ReactDOM.findDOMNode(this.refs.Scope).value.trim();
// <div className='form-group'>
// <label
diff --git a/web/react/components/center_panel.jsx b/web/react/components/center_panel.jsx
new file mode 100644
index 000000000..b871fe81a
--- /dev/null
+++ b/web/react/components/center_panel.jsx
@@ -0,0 +1,54 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var CreatePost = require('../components/create_post.jsx');
+var PostsViewContainer = require('../components/posts_view_container.jsx');
+var ChannelHeader = require('../components/channel_header.jsx');
+var Navbar = require('../components/navbar.jsx');
+var FileUploadOverlay = require('../components/file_upload_overlay.jsx');
+
+export default class CenterPanel extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+ render() {
+ return (
+ <div className='inner__wrap channel__wrap'>
+ <div className='row header'>
+ <div id='navbar'>
+ <Navbar/>
+ </div>
+ </div>
+ <div className='row main'>
+ <FileUploadOverlay
+ id='file_upload_overlay'
+ overlayType='center'
+ />
+ <div
+ id='app-content'
+ className='app__content'
+ >
+ <div id='channel-header'>
+ <ChannelHeader />
+ </div>
+ <div id='post-list'>
+ <PostsViewContainer />
+ </div>
+ <div
+ className='post-create__container'
+ id='post-create'
+ >
+ <CreatePost />
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+CenterPanel.defaultProps = {
+};
+
+CenterPanel.propTypes = {
+};
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx
index 101fd85e5..20f106f30 100644
--- a/web/react/components/channel_header.jsx
+++ b/web/react/components/channel_header.jsx
@@ -4,6 +4,7 @@
const ChannelStore = require('../stores/channel_store.jsx');
const UserStore = require('../stores/user_store.jsx');
const SearchStore = require('../stores/search_store.jsx');
+const PreferenceStore = require('../stores/preference_store.jsx');
const NavbarSearchBox = require('./search_bar.jsx');
const AsyncClient = require('../utils/async_client.jsx');
const Client = require('../utils/client.jsx');
@@ -46,12 +47,14 @@ export default class ChannelHeader extends React.Component {
ChannelStore.addExtraInfoChangeListener(this.onListenerChange);
SearchStore.addSearchChangeListener(this.onListenerChange);
UserStore.addChangeListener(this.onListenerChange);
+ PreferenceStore.addChangeListener(this.onListenerChange);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onListenerChange);
ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
SearchStore.removeSearchChangeListener(this.onListenerChange);
- UserStore.addChangeListener(this.onListenerChange);
+ UserStore.removeChangeListener(this.onListenerChange);
+ PreferenceStore.removeChangeListener(this.onListenerChange);
}
onListenerChange() {
const newState = this.getStateFromStores();
@@ -134,7 +137,7 @@ export default class ChannelHeader extends React.Component {
} else {
contact = this.state.users[0];
}
- channelTitle = contact.nickname || contact.username;
+ channelTitle = Utils.displayUsername(contact.id);
}
}
diff --git a/web/react/components/channel_view.jsx b/web/react/components/channel_view.jsx
new file mode 100644
index 000000000..beafa7d63
--- /dev/null
+++ b/web/react/components/channel_view.jsx
@@ -0,0 +1,43 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var CenterPanel = require('../components/center_panel.jsx');
+var Sidebar = require('../components/sidebar.jsx');
+var SidebarRight = require('../components/sidebar_right.jsx');
+var SidebarRightMenu = require('../components/sidebar_right_menu.jsx');
+
+export default class ChannelView extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+ render() {
+ return (
+ <div className='container-fluid'>
+ <div
+ className='sidebar--right'
+ id='sidebar-right'
+ >
+ <SidebarRight/>
+ </div>
+ <div
+ className='sidebar--menu'
+ id='sidebar-menu'
+ >
+ <SidebarRightMenu/>
+ </div>
+ <div
+ className='sidebar--left'
+ id='sidebar-left'
+ >
+ <Sidebar/>
+ </div>
+ <CenterPanel />
+ </div>
+ );
+ }
+}
+ChannelView.defaultProps = {
+};
+
+ChannelView.propTypes = {
+};
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index cdbc3bc6d..7c601af4b 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -176,6 +176,7 @@ export default class CreatePost extends React.Component {
PostStore.storePendingPost(post);
PostStore.storeDraft(channel.id, null);
+ PostStore.jumpPostsViewToBottom();
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
Client.createPost(post, channel,
diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx
index 2abb3f151..ef32baa7d 100644
--- a/web/react/components/edit_post_modal.jsx
+++ b/web/react/components/edit_post_modal.jsx
@@ -120,7 +120,7 @@ export default class EditPostModal extends React.Component {
PreferenceStore.addChangeListener(this.onPreferenceChange);
}
componentWillUnmount() {
- PostStore.removeEditPostListener(this.handleEditPostEvent);
+ PostStore.removeEditPostListner(this.handleEditPostEvent);
PreferenceStore.removeChangeListener(this.onPreferenceChange);
}
render() {
diff --git a/web/react/components/find_team.jsx b/web/react/components/find_team.jsx
index e324f3666..bd3c11973 100644
--- a/web/react/components/find_team.jsx
+++ b/web/react/components/find_team.jsx
@@ -50,7 +50,7 @@ export default class FindTeam extends React.Component {
if (this.state.sent) {
return (
<div>
- <h4>{'Find Your team'}</h4>
+ <h4>{'Find Your teams'}</h4>
<p>{'An email was sent with links to any teams to which you are a member.'}</p>
</div>
);
diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx
index dedac8951..c3c5b3e0b 100644
--- a/web/react/components/post.jsx
+++ b/web/react/components/post.jsx
@@ -204,7 +204,6 @@ export default class Post extends React.Component {
posts={posts}
handleCommentClick={this.handleCommentClick}
retryPost={this.retryPost}
- resize={this.props.resize}
/>
<PostInfo
ref='info'
@@ -228,6 +227,5 @@ Post.propTypes = {
sameUser: React.PropTypes.bool,
sameRoot: React.PropTypes.bool,
hideProfilePic: React.PropTypes.bool,
- isLastComment: React.PropTypes.bool,
- resize: React.PropTypes.func
+ isLastComment: React.PropTypes.bool
};
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index 7138e2cb4..e1f495d54 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -50,7 +50,6 @@ export default class PostBody extends React.Component {
componentDidUpdate() {
this.parseEmojis();
- this.props.resize();
}
componentWillReceiveProps(nextProps) {
@@ -338,6 +337,5 @@ PostBody.propTypes = {
post: React.PropTypes.object.isRequired,
parentPost: React.PropTypes.object,
retryPost: React.PropTypes.func.isRequired,
- handleCommentClick: React.PropTypes.func.isRequired,
- resize: React.PropTypes.func.isRequired
+ handleCommentClick: React.PropTypes.func.isRequired
};
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index 6937ec216..a01d842e5 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -3,10 +3,9 @@
var UserStore = require('../stores/user_store.jsx');
var utils = require('../utils/utils.jsx');
+var TimeSince = require('./time_since.jsx');
var Constants = require('../utils/constants.jsx');
-var Tooltip = ReactBootstrap.Tooltip;
-var OverlayTrigger = ReactBootstrap.OverlayTrigger;
export default class PostInfo extends React.Component {
constructor(props) {
@@ -126,7 +125,7 @@ export default class PostInfo extends React.Component {
lastCommentClass = ' comment-icon__container__show';
}
- if (this.props.commentCount >= 1 && post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING) {
+ if (this.props.commentCount >= 1 && post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING && post.state !== Constants.POST_DELETED) {
comments = (
<a
href='#'
@@ -144,21 +143,12 @@ export default class PostInfo extends React.Component {
var dropdown = this.createDropdown();
- let tooltip = <Tooltip id={post.id + 'tooltip'}>{`${utils.displayDate(post.create_at)} at ${utils.displayTime(post.create_at)}`}</Tooltip>;
-
return (
<ul className='post-header post-info'>
<li className='post-header-col'>
- <OverlayTrigger
- delayShow={500}
- container={this}
- placement='top'
- overlay={tooltip}
- >
- <time className='post-profile-time'>
- {utils.displayDateTime(post.create_at)}
- </time>
- </OverlayTrigger>
+ <TimeSince
+ eventTime={post.create_at}
+ />
</li>
<li className='post-header-col post-header__reply'>
<div className='dropdown'>
diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx
deleted file mode 100644
index b9741bac4..000000000
--- a/web/react/components/post_list.jsx
+++ /dev/null
@@ -1,759 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-const Post = require('./post.jsx');
-const UserProfile = require('./user_profile.jsx');
-const AsyncClient = require('../utils/async_client.jsx');
-const LoadingScreen = require('./loading_screen.jsx');
-
-const PostStore = require('../stores/post_store.jsx');
-const ChannelStore = require('../stores/channel_store.jsx');
-const UserStore = require('../stores/user_store.jsx');
-const TeamStore = require('../stores/team_store.jsx');
-const SocketStore = require('../stores/socket_store.jsx');
-const PreferenceStore = require('../stores/preference_store.jsx');
-
-const Utils = require('../utils/utils.jsx');
-const Client = require('../utils/client.jsx');
-const Constants = require('../utils/constants.jsx');
-const ActionTypes = Constants.ActionTypes;
-const SocketEvents = Constants.SocketEvents;
-
-const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-
-export default class PostList extends React.Component {
- constructor(props) {
- super(props);
-
- this.gotMorePosts = false;
- this.scrolled = false;
- this.prevScrollTop = 0;
- this.seenNewMessages = false;
- this.isUserScroll = true;
- this.userHasSeenNew = false;
- this.loadInProgress = false;
-
- this.onChange = this.onChange.bind(this);
- this.onTimeChange = this.onTimeChange.bind(this);
- this.onSocketChange = this.onSocketChange.bind(this);
- this.createChannelIntroMessage = this.createChannelIntroMessage.bind(this);
- this.loadMorePosts = this.loadMorePosts.bind(this);
- this.loadFirstPosts = this.loadFirstPosts.bind(this);
- this.activate = this.activate.bind(this);
- this.deactivate = this.deactivate.bind(this);
- this.handleResize = this.handleResize.bind(this);
- this.resizePostList = this.resizePostList.bind(this);
- this.updateScroll = this.updateScroll.bind(this);
-
- const state = this.getStateFromStores(props.channelId);
- state.numToDisplay = Constants.POST_CHUNK_SIZE;
- state.isFirstLoadComplete = false;
- state.windowHeight = Utils.windowHeight();
-
- this.state = state;
- }
- getStateFromStores(id) {
- var postList = PostStore.getPosts(id);
-
- if (postList != null) {
- var deletedPosts = PostStore.getUnseenDeletedPosts(id);
-
- if (deletedPosts && Object.keys(deletedPosts).length > 0) {
- for (var pid in deletedPosts) {
- if (deletedPosts.hasOwnProperty(pid)) {
- postList.posts[pid] = deletedPosts[pid];
- postList.order.unshift(pid);
- }
- }
-
- postList.order.sort((a, b) => {
- if (postList.posts[a].create_at > postList.posts[b].create_at) {
- return -1;
- }
- if (postList.posts[a].create_at < postList.posts[b].create_at) {
- return 1;
- }
- return 0;
- });
- }
-
- var pendingPostList = PostStore.getPendingPosts(id);
-
- if (pendingPostList) {
- postList.order = pendingPostList.order.concat(postList.order);
- for (var ppid in pendingPostList.posts) {
- if (pendingPostList.posts.hasOwnProperty(ppid)) {
- postList.posts[ppid] = pendingPostList.posts[ppid];
- }
- }
- }
- }
-
- return {
- postList
- };
- }
- componentDidMount() {
- window.onload = () => this.scrollToBottom();
- if (this.props.isActive) {
- this.activate();
- this.loadFirstPosts(this.props.channelId);
- }
- }
- componentWillUnmount() {
- this.deactivate();
- }
- activate() {
- this.gotMorePosts = false;
- this.scrolled = false;
- this.prevScrollTop = 0;
- this.seenNewMessages = false;
- this.isUserScroll = true;
- this.userHasSeenNew = false;
-
- PostStore.clearUnseenDeletedPosts(this.props.channelId);
- PostStore.addChangeListener(this.onChange);
- UserStore.addStatusesChangeListener(this.onTimeChange);
- PreferenceStore.addChangeListener(this.onTimeChange);
- SocketStore.addChangeListener(this.onSocketChange);
-
- const postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
-
- window.addEventListener('resize', this.handleResize);
-
- postHolder.on('scroll', () => {
- const position = postHolder.scrollTop() + postHolder.height() + 14;
- const bottom = postHolder[0].scrollHeight;
-
- if (position >= bottom) {
- this.scrolled = false;
- } else {
- this.scrolled = true;
- }
-
- if (this.isUserScroll) {
- this.userHasSeenNew = true;
- }
- this.isUserScroll = true;
-
- $('.top-visible-post').removeClass('top-visible-post');
-
- $(ReactDOM.findDOMNode(this.refs.postlistcontent)).children().each(function select() {
- if ($(this).position().top + $(this).height() / 2 > 0) {
- $(this).addClass('top-visible-post');
- return false;
- }
- });
- });
-
- $('.post-list__content div .post').removeClass('post--last');
- $('.post-list__content div:last-child .post').addClass('post--last');
-
- if (!this.state.isFirstLoadComplete) {
- this.loadFirstPosts(this.props.channelId);
- }
-
- this.resizePostList();
- this.onChange();
- this.scrollToBottom();
- }
- deactivate() {
- PostStore.removeChangeListener(this.onChange);
- UserStore.removeStatusesChangeListener(this.onTimeChange);
- SocketStore.removeChangeListener(this.onSocketChange);
- PreferenceStore.removeChangeListener(this.onTimeChange);
- $('body').off('click.userpopover');
-
- window.removeEventListener('resize', this.handleResize);
-
- var postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
- postHolder.off('scroll');
- }
- componentDidUpdate(prevProps, prevState) {
- if (!this.props.isActive) {
- return;
- }
-
- if (prevState.windowHeight !== this.state.windowHeight) {
- this.resizePostList();
- if (!this.scrolled) {
- this.scrollToBottom();
- }
- }
-
- $('.post-list__content div .post').removeClass('post--last');
- $('.post-list__content div:last-child .post').addClass('post--last');
-
- if (this.state.postList == null || prevState.postList == null) {
- this.scrollToBottom();
- return;
- }
-
- var order = this.state.postList.order || [];
- var posts = this.state.postList.posts || {};
- var oldOrder = prevState.postList.order || [];
- var oldPosts = prevState.postList.posts || {};
- var userId = UserStore.getCurrentId();
- var firstPost = posts[order[0]] || {};
- var isNewPost = oldOrder.indexOf(order[0]) === -1;
-
- if (this.props.isActive && !prevProps.isActive) {
- this.scrollToBottom();
- } else if (oldOrder.length === 0) {
- this.scrollToBottom();
-
- // the user is scrolled to the bottom
- } else if (!this.scrolled) {
- this.scrollToBottom();
-
- // there's a new post and
- // it's by the user (and not from their webhook) and not a comment
- } else if (isNewPost &&
- userId === firstPost.user_id &&
- !firstPost.props.from_webhook &&
- !Utils.isComment(firstPost)) {
- this.scrollToBottom(true);
-
- // the user clicked 'load more messages'
- } else if (this.gotMorePosts && oldOrder.length > 0) {
- let index;
- if (prevState.numToDisplay >= oldOrder.length) {
- index = oldOrder.length - 1;
- } else {
- index = prevState.numToDisplay;
- }
- const lastPost = oldPosts[oldOrder[index]];
- $('#post_' + lastPost.id)[0].scrollIntoView();
- this.gotMorePosts = false;
- } else {
- this.scrollTo(this.prevScrollTop);
- }
- }
- componentWillUpdate() {
- var postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
- this.prevScrollTop = postHolder.scrollTop();
- }
- componentWillReceiveProps(nextProps) {
- if (nextProps.isActive === true && this.props.isActive === false) {
- this.activate();
- } else if (nextProps.isActive === false && this.props.isActive === true) {
- this.deactivate();
- }
- }
- updateScroll() {
- if (!this.scrolled) {
- this.scrollToBottom();
- }
- }
- handleResize() {
- this.setState({
- windowHeight: Utils.windowHeight()
- });
- }
- resizePostList() {
- const postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
- if ($('#create_post').length > 0) {
- const height = this.state.windowHeight - $('#create_post').height() - $('#error_bar').outerHeight() - 50;
- postHolder.css('height', height + 'px');
- }
- }
- scrollTo(val) {
- this.isUserScroll = false;
- var postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
- postHolder[0].scrollTop = val;
- }
- scrollToBottom(force) {
- this.isUserScroll = false;
- var postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
- if ($('#new_message_' + this.props.channelId)[0] && !this.userHasSeenNew && !force) {
- $('#new_message_' + this.props.channelId)[0].scrollIntoView();
- } else {
- postHolder.addClass('hide-scroll');
- postHolder[0].scrollTop = postHolder[0].scrollHeight;
- postHolder.removeClass('hide-scroll');
- }
- }
- loadFirstPosts(id) {
- if (this.loadInProgress) {
- return;
- }
-
- if (this.props.channelId == null) {
- return;
- }
-
- this.loadInProgress = true;
- Client.getPosts(
- id,
- PostStore.getLatestUpdate(id),
- () => {
- this.loadInProgress = false;
- this.setState({isFirstLoadComplete: true});
- },
- () => {
- this.loadInProgress = false;
- this.setState({isFirstLoadComplete: true});
- }
- );
- }
- onChange() {
- var newState = this.getStateFromStores(this.props.channelId);
-
- if (!Utils.areStatesEqual(newState.postList, this.state.postList)) {
- this.setState(newState);
- }
- }
- onSocketChange(msg) {
- if (msg.action === SocketEvents.POST_DELETED) {
- var activeRoot = $(document.activeElement).closest('.comment-create-body')[0];
- var activeRootPostId = '';
- if (activeRoot && activeRoot.id.length > 0) {
- activeRootPostId = activeRoot.id;
- }
-
- if (activeRootPostId === msg.props.post_id && UserStore.getCurrentId() !== msg.user_id) {
- $('#post_deleted').modal('show');
- }
- }
- }
- onTimeChange() {
- if (!this.state.postList) {
- return;
- }
-
- for (var id in this.state.postList.posts) {
- if (!this.refs[id]) {
- continue;
- }
- this.refs[id].forceUpdateInfo();
- }
- }
- createDMIntroMessage(channel) {
- var teammate = Utils.getDirectTeammate(channel.id);
-
- if (teammate) {
- var teammateName = teammate.username;
- if (teammate.nickname.length > 0) {
- teammateName = teammate.nickname;
- }
-
- return (
- <div className='channel-intro'>
- <div className='post-profile-img__container channel-intro-img'>
- <img
- className='post-profile-img'
- src={'/api/v1/users/' + teammate.id + '/image?time=' + teammate.update_at + '&' + Utils.getSessionIndex()}
- height='50'
- width='50'
- />
- </div>
- <div className='channel-intro-profile'>
- <strong><UserProfile userId={teammate.id} /></strong>
- </div>
- <p className='channel-intro-text'>
- {'This is the start of your direct message history with ' + teammateName + '.'}<br/>
- {'Direct messages and files shared here are not shown to people outside this area.'}
- </p>
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-header={channel.header}
- data-title={channel.display_name}
- data-channelid={channel.id}
- >
- <i className='fa fa-pencil'></i>{'Set a header'}
- </a>
- </div>
- );
- }
-
- return (
- <div className='channel-intro'>
- <p className='channel-intro-text'>{'This is the start of your direct message history with this teammate. Direct messages and files shared here are not shown to people outside this area.'}</p>
- </div>
- );
- }
- createChannelIntroMessage(channel) {
- if (channel.type === 'D') {
- return this.createDMIntroMessage(channel);
- } else if (ChannelStore.isDefault(channel)) {
- return this.createDefaultIntroMessage(channel);
- } else if (channel.name === Constants.OFFTOPIC_CHANNEL) {
- return this.createOffTopicIntroMessage(channel);
- } else if (channel.type === 'O' || channel.type === 'P') {
- return this.createStandardIntroMessage(channel);
- }
- }
- createDefaultIntroMessage(channel) {
- const team = TeamStore.getCurrent();
- let inviteModalLink;
- if (team.type === Constants.INVITE_TEAM) {
- inviteModalLink = (
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#invite_member'
- >
- <i className='fa fa-user-plus'></i>{'Invite others to this team'}
- </a>
- );
- } else {
- inviteModalLink = (
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#get_link'
- data-title='Team Invite'
- data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + team.id}
- >
- <i className='fa fa-user-plus'></i>{'Invite others to this team'}
- </a>
- );
- }
-
- return (
- <div className='channel-intro'>
- <h4 className='channel-intro__title'>{'Beginning of ' + channel.display_name}</h4>
- <p className='channel-intro__content'>
- <strong>{'Welcome to ' + channel.display_name + '!'}</strong>
- <br/><br/>
- {'This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.'}
- </p>
- {inviteModalLink}
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-header={channel.header}
- data-title={channel.display_name}
- data-channelid={channel.id}
- >
- <i className='fa fa-pencil'></i>{'Set a header'}
- </a>
- <br/>
- </div>
- );
- }
- createOffTopicIntroMessage(channel) {
- return (
- <div className='channel-intro'>
- <h4 className='channel-intro__title'>{'Beginning of ' + channel.display_name}</h4>
- <p className='channel-intro__content'>
- {'This is the start of ' + channel.display_name + ', a channel for non-work-related conversations.'}
- <br/>
- </p>
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-header={channel.header}
- data-title={channel.display_name}
- data-channelid={channel.id}
- >
- <i className='fa fa-pencil'></i>{'Set a header'}
- </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>
- );
- }
- getChannelCreator(channel) {
- if (channel.creator_id.length > 0) {
- var creator = UserStore.getProfile(channel.creator_id);
- if (creator) {
- return creator.username;
- }
- }
-
- var members = ChannelStore.getExtraInfo(channel.id).members;
- for (var i = 0; i < members.length; i++) {
- if (Utils.isAdmin(members[i].roles)) {
- return members[i].username;
- }
- }
- }
- createStandardIntroMessage(channel) {
- var uiName = channel.display_name;
- var creatorName = '';
-
- var uiType;
- var memberMessage;
- if (channel.type === 'P') {
- uiType = 'private group';
- memberMessage = ' Only invited members can see this private group.';
- } else {
- uiType = 'channel';
- memberMessage = ' Any member can join and read this channel.';
- }
-
- var createMessage;
- 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 (
- <div className='channel-intro'>
- <h4 className='channel-intro__title'>{'Beginning of ' + uiName}</h4>
- <p className='channel-intro__content'>
- {createMessage}
- {memberMessage}
- <br/>
- </p>
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-header={channel.header}
- data-title={channel.display_name}
- data-channelid={channel.id}
- >
- <i className='fa fa-pencil'></i>{'Set a header'}
- </a>
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#channel_invite'
- >
- <i className='fa fa-user-plus'></i>{'Invite others to this ' + uiType}
- </a>
- </div>
- );
- }
- createPosts(posts, order) {
- var postCtls = [];
- var previousPostDay = new Date(0);
- var userId = UserStore.getCurrentId();
-
- var renderedLastViewed = false;
- var lastViewed = Number.MAX_VALUE;
-
- if (ChannelStore.getMember(this.props.channelId) != null) {
- lastViewed = ChannelStore.getMember(this.props.channelId).last_viewed_at;
- }
-
- var numToDisplay = this.state.numToDisplay;
- if (order.length - 1 < numToDisplay) {
- numToDisplay = order.length - 1;
- }
-
- for (var i = numToDisplay; i >= 0; i--) {
- var post = posts[order[i]];
- var parentPost = posts[post.parent_id];
-
- var sameUser = false;
- var sameRoot = false;
- var hideProfilePic = false;
- var prevPost = posts[order[i + 1]];
-
- if (prevPost) {
- sameUser = prevPost.user_id === post.user_id && post.create_at - prevPost.create_at <= 1000 * 60 * 5;
-
- sameRoot = Utils.isComment(post) && (prevPost.id === post.root_id || prevPost.root_id === post.root_id);
-
- // 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
- // it is the last comment if it is last post in the channel or the next post has a different root post
- var isLastComment = Utils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id);
-
- var postCtl = (
- <Post
- key={post.id + 'postKey'}
- ref={post.id}
- sameUser={sameUser}
- sameRoot={sameRoot}
- post={post}
- parentPost={parentPost}
- posts={posts}
- hideProfilePic={hideProfilePic}
- isLastComment={isLastComment}
- resize={this.updateScroll}
- />
- );
-
- const currentPostDay = Utils.getDateForUnixTicks(post.create_at);
- if (currentPostDay.toDateString() !== previousPostDay.toDateString()) {
- postCtls.push(
- <div
- key={currentPostDay.toDateString()}
- className='date-separator'
- >
- <hr className='separator__hr' />
- <div className='separator__text'>{currentPostDay.toDateString()}</div>
- </div>
- );
- }
-
- if (post.user_id !== userId && post.create_at > lastViewed && !renderedLastViewed) {
- renderedLastViewed = true;
-
- // Temporary fix to solve ie11 rendering issue
- let newSeparatorId = '';
- if (!Utils.isBrowserIE()) {
- newSeparatorId = 'new_message_' + this.props.channelId;
- }
- postCtls.push(
- <div
- id={newSeparatorId}
- key='unviewed'
- className='new-separator'
- >
- <hr
- className='separator__hr'
- />
- <div className='separator__text'>{'New Messages'}</div>
- </div>
- );
- }
- postCtls.push(postCtl);
- previousPostDay = currentPostDay;
- }
-
- return postCtls;
- }
- loadMorePosts() {
- if (this.state.postList == null) {
- return;
- }
-
- var posts = this.state.postList.posts;
- var order = this.state.postList.order;
- var channelId = this.props.channelId;
-
- $(ReactDOM.findDOMNode(this.refs.loadmore)).text('Retrieving more messages...');
-
- Client.getPostsPage(
- channelId,
- order.length,
- Constants.POST_CHUNK_SIZE,
- function success(data) {
- $(ReactDOM.findDOMNode(this.refs.loadmore)).text('Load more messages');
- this.gotMorePosts = true;
- this.setState({numToDisplay: this.state.numToDisplay + Constants.POST_CHUNK_SIZE});
-
- if (!data) {
- return;
- }
-
- if (data.order.length === 0) {
- return;
- }
-
- var postList = {};
- postList.posts = $.extend(posts, data.posts);
- postList.order = order.concat(data.order);
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_POSTS,
- id: channelId,
- post_list: postList
- });
-
- Client.getProfiles();
- }.bind(this),
- function fail(err) {
- $(ReactDOM.findDOMNode(this.refs.loadmore)).text('Load more messages');
- AsyncClient.dispatchError(err, 'getPosts');
- }.bind(this)
- );
- }
- render() {
- var order = [];
- var posts;
- var channel = ChannelStore.get(this.props.channelId);
-
- if (this.state.postList != null) {
- posts = this.state.postList.posts;
- order = this.state.postList.order;
- }
-
- var moreMessages = <p className='beginning-messages-text'>{'Beginning of Channel'}</p>;
- if (channel != null) {
- if (order.length >= this.state.numToDisplay) {
- moreMessages = (
- <a
- ref='loadmore'
- className='more-messages-text theme'
- href='#'
- onClick={this.loadMorePosts}
- >
- {'Load more messages'}
- </a>
- );
- } else {
- moreMessages = this.createChannelIntroMessage(channel);
- }
- }
-
- var postCtls = [];
- if (posts && this.state.isFirstLoadComplete) {
- postCtls = this.createPosts(posts, order);
- } else {
- postCtls.push(
- <LoadingScreen
- position='absolute'
- key='loading'
- />);
- }
-
- var activeClass = '';
- if (!this.props.isActive) {
- activeClass = 'inactive';
- }
-
- return (
- <div
- ref='postlist'
- className={'post-list-holder-by-time ' + activeClass}
- >
- <div className='post-list__table'>
- <div
- ref='postlistcontent'
- className='post-list__content'
- >
- {moreMessages}
- {postCtls}
- </div>
- </div>
- </div>
- );
- }
-}
-
-PostList.defaultProps = {
- isActive: false,
- channelId: null
-};
-PostList.propTypes = {
- isActive: React.PropTypes.bool,
- channelId: React.PropTypes.string
-};
diff --git a/web/react/components/post_list_container.jsx b/web/react/components/post_list_container.jsx
deleted file mode 100644
index 09cee6218..000000000
--- a/web/react/components/post_list_container.jsx
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-const PostList = require('./post_list.jsx');
-const ChannelStore = require('../stores/channel_store.jsx');
-
-export default class PostListContainer extends React.Component {
- constructor() {
- super();
-
- this.onChange = this.onChange.bind(this);
- this.onLeave = this.onLeave.bind(this);
-
- let currentChannelId = ChannelStore.getCurrentId();
- if (currentChannelId) {
- this.state = {currentChannelId: currentChannelId, postLists: [currentChannelId]};
- } else {
- this.state = {currentChannelId: null, postLists: []};
- }
- }
- componentDidMount() {
- ChannelStore.addChangeListener(this.onChange);
- ChannelStore.addLeaveListener(this.onLeave);
- }
- onChange() {
- let channelId = ChannelStore.getCurrentId();
- if (channelId === this.state.currentChannelId) {
- return;
- }
-
- let postLists = this.state.postLists;
- if (postLists.indexOf(channelId) === -1) {
- postLists.push(channelId);
- }
- this.setState({currentChannelId: channelId, postLists: postLists});
- }
- onLeave(id) {
- let postLists = this.state.postLists;
- var index = postLists.indexOf(id);
- if (index !== -1) {
- postLists.splice(index, 1);
- }
- }
- render() {
- let postLists = this.state.postLists;
- let channelId = this.state.currentChannelId;
-
- let postListCtls = [];
- for (let i = 0; i <= this.state.postLists.length - 1; i++) {
- postListCtls.push(
- <PostList
- key={'postlistkey' + i}
- channelId={postLists[i]}
- isActive={postLists[i] === channelId}
- />
- );
- }
-
- return (
- <div>{postListCtls}</div>
- );
- }
-}
diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx
new file mode 100644
index 000000000..f5a492b85
--- /dev/null
+++ b/web/react/components/posts_view.jsx
@@ -0,0 +1,297 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const UserStore = require('../stores/user_store.jsx');
+const Utils = require('../utils/utils.jsx');
+const Post = require('./post.jsx');
+
+export default class PostsView extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleScroll = this.handleScroll.bind(this);
+ this.isAtBottom = this.isAtBottom.bind(this);
+ this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
+ this.createPosts = this.createPosts.bind(this);
+ this.updateScrolling = this.updateScrolling.bind(this);
+ this.handleResize = this.handleResize.bind(this);
+
+ this.jumpToPostNode = null;
+ this.wasAtBottom = true;
+ this.scrollHeight = 0;
+ }
+ static get SCROLL_TYPE_FREE() {
+ return 1;
+ }
+ static get SCROLL_TYPE_BOTTOM() {
+ return 2;
+ }
+ static get SIDEBAR_OPEN() {
+ return 3;
+ }
+ isAtBottom() {
+ return ((this.refs.postlist.scrollHeight - this.refs.postlist.scrollTop) === this.refs.postlist.clientHeight);
+ }
+ handleScroll() {
+ // HACK FOR RHS -- REMOVE WHEN RHS DIES
+ const childNodes = this.refs.postlistcontent.childNodes;
+ for (let i = 0; i < childNodes.length; i++) {
+ // If the node is 1/3 down the page
+ if (childNodes[i].offsetTop > (this.refs.postlist.scrollTop + (this.refs.postlist.offsetHeight / 3))) {
+ this.jumpToPostNode = childNodes[i];
+ break;
+ }
+ }
+ this.wasAtBottom = this.isAtBottom();
+
+ // --- --------
+
+ this.props.postViewScrolled(this.isAtBottom());
+ this.prevScrollHeight = this.refs.postlist.scrollHeight;
+ }
+ loadMorePostsTop() {
+ this.props.loadMorePostsTopClicked();
+ }
+ createPosts(posts, order) {
+ const postCtls = [];
+ let previousPostDay = new Date(0);
+ const userId = UserStore.getCurrentId();
+
+ let renderedLastViewed = false;
+
+ let numToDisplay = this.props.numPostsToDisplay;
+ if (order.length - 1 < numToDisplay) {
+ numToDisplay = order.length - 1;
+ }
+
+ for (let i = numToDisplay; i >= 0; i--) {
+ const post = posts[order[i]];
+ const parentPost = posts[post.parent_id];
+ const prevPost = posts[order[i + 1]];
+
+ let sameUser = false;
+ let sameRoot = false;
+ let hideProfilePic = false;
+
+ if (prevPost) {
+ sameUser = prevPost.user_id === post.user_id && post.create_at - prevPost.create_at <= 1000 * 60 * 5;
+
+ sameRoot = Utils.isComment(post) && (prevPost.id === post.root_id || prevPost.root_id === post.root_id);
+
+ // 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
+ // it is the last comment if it is last post in the channel or the next post has a different root post
+ var isLastComment = Utils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id);
+
+ var postCtl = (
+ <Post
+ key={post.id + 'postKey'}
+ ref={post.id}
+ sameUser={sameUser}
+ sameRoot={sameRoot}
+ post={post}
+ parentPost={parentPost}
+ posts={posts}
+ hideProfilePic={hideProfilePic}
+ isLastComment={isLastComment}
+ />
+ );
+
+ const currentPostDay = Utils.getDateForUnixTicks(post.create_at);
+ if (currentPostDay.toDateString() !== previousPostDay.toDateString()) {
+ postCtls.push(
+ <div
+ key={currentPostDay.toDateString()}
+ className='date-separator'
+ >
+ <hr className='separator__hr' />
+ <div className='separator__text'>{currentPostDay.toDateString()}</div>
+ </div>
+ );
+ }
+
+ if (post.user_id !== userId &&
+ this.props.messageSeparatorTime !== 0 &&
+ post.create_at > this.props.messageSeparatorTime &&
+ !renderedLastViewed) {
+ renderedLastViewed = true;
+
+ // Temporary fix to solve ie11 rendering issue
+ let newSeparatorId = '';
+ if (!Utils.isBrowserIE()) {
+ newSeparatorId = 'new_message_' + post.id;
+ }
+ postCtls.push(
+ <div
+ id={newSeparatorId}
+ key='unviewed'
+ className='new-separator'
+ >
+ <hr
+ className='separator__hr'
+ />
+ <div className='separator__text'>{'New Messages'}</div>
+ </div>
+ );
+ }
+ postCtls.push(postCtl);
+ previousPostDay = currentPostDay;
+ }
+
+ return postCtls;
+ }
+ updateScrolling() {
+ if (this.props.scrollType === PostsView.SCROLL_TYPE_BOTTOM) {
+ window.requestAnimationFrame(() => {
+ this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
+ });
+ } else if (this.props.scrollType === PostsView.SCROLL_TYPE_POST && this.props.scrollPost) {
+ window.requestAnimationFrame(() => {
+ const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPost]);
+ postNode.scrollIntoView();
+ if (this.refs.postlist.scrollTop === postNode.offsetTop) {
+ this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3);
+ } else {
+ this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3) + (this.refs.postlist.scrollTop - postNode.offsetTop);
+ }
+ });
+ } else if (this.props.scrollType === PostsView.SIDEBAR_OPEN) {
+ // If we are at the bottom then stay there
+ if (this.wasAtBottom) {
+ this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
+ } else {
+ window.requestAnimationFrame(() => {
+ this.jumpToPostNode.scrollIntoView();
+ if (this.refs.postlist.scrollTop === this.jumpToPostNode.offsetTop) {
+ this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3);
+ } else {
+ this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3) + (this.refs.postlist.scrollTop - this.jumpToPostNode.offsetTop);
+ }
+ });
+ }
+ } else if (this.refs.postlist.scrollHeight !== this.prevScrollHeight) {
+ window.requestAnimationFrame(() => {
+ this.refs.postlist.scrollTop += (this.refs.postlist.scrollHeight - this.prevScrollHeight);
+ });
+ }
+ }
+ handleResize() {
+ this.updateScrolling();
+ }
+ componentDidMount() {
+ this.updateScrolling();
+ window.addEventListener('resize', this.handleResize);
+ }
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.handleResize);
+ }
+ componentDidUpdate() {
+ this.updateScrolling();
+ }
+ shouldComponentUpdate(nextProps) {
+ if (this.props.isActive !== nextProps.isActive) {
+ return true;
+ }
+ if (this.props.postList !== nextProps.postList) {
+ return true;
+ }
+ if (this.props.scrollPost !== nextProps.scrollPost) {
+ return true;
+ }
+ if (this.props.scrollType !== nextProps.scrollType && nextProps.scrollType !== PostsView.SCROLL_TYPE_FREE) {
+ return true;
+ }
+ if (this.props.numPostsToDisplay !== nextProps.numPostsToDisplay) {
+ return true;
+ }
+ if (this.props.messageSeparatorTime !== nextProps.messageSeparatorTime) {
+ return true;
+ }
+ if (!Utils.areStatesEqual(this.props.postList, nextProps.postList)) {
+ return true;
+ }
+
+ return false;
+ }
+ render() {
+ let posts = [];
+ let order = [];
+ let moreMessages;
+ let postElements;
+ let activeClass = 'inactive';
+ if (this.props.postList != null) {
+ posts = this.props.postList.posts;
+ order = this.props.postList.order;
+
+ // Create intro message or top loadmore link
+ if (order.length >= this.props.numPostsToDisplay) {
+ moreMessages = (
+ <a
+ ref='loadmore'
+ className='more-messages-text theme'
+ href='#'
+ onClick={this.loadMorePostsTop}
+ >
+ {'Load more messages'}
+ </a>
+ );
+ } else {
+ moreMessages = this.props.introText;
+ }
+
+ // Create post elements
+ postElements = this.createPosts(posts, order);
+
+ // Show ourselves if we are marked active
+ if (this.props.isActive) {
+ activeClass = '';
+ }
+ }
+
+ return (
+ <div
+ ref='postlist'
+ className={'post-list-holder-by-time ' + activeClass}
+ onScroll={this.handleScroll}
+ >
+ <div className='post-list__table'>
+ <div
+ ref='postlistcontent'
+ className='post-list__content'
+ >
+ {moreMessages}
+ {postElements}
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+PostsView.defaultProps = {
+};
+
+PostsView.propTypes = {
+ isActive: React.PropTypes.bool,
+ postList: React.PropTypes.object,
+ scrollPost: React.PropTypes.string,
+ scrollType: React.PropTypes.number,
+ postViewScrolled: React.PropTypes.func.isRequired,
+ loadMorePostsTopClicked: React.PropTypes.func.isRequired,
+ numPostsToDisplay: React.PropTypes.number,
+ introText: React.PropTypes.element,
+ messageSeparatorTime: React.PropTypes.number
+};
diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx
new file mode 100644
index 000000000..9eda2a158
--- /dev/null
+++ b/web/react/components/posts_view_container.jsx
@@ -0,0 +1,264 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const PostsView = require('./posts_view.jsx');
+const ChannelStore = require('../stores/channel_store.jsx');
+const PostStore = require('../stores/post_store.jsx');
+const Constants = require('../utils/constants.jsx');
+const ActionTypes = Constants.ActionTypes;
+const Utils = require('../utils/utils.jsx');
+const Client = require('../utils/client.jsx');
+const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
+const AsyncClient = require('../utils/async_client.jsx');
+const LoadingScreen = require('./loading_screen.jsx');
+
+import {createChannelIntroMessage} from '../utils/channel_intro_mssages.jsx';
+
+export default class PostsViewContainer extends React.Component {
+ constructor() {
+ super();
+
+ this.onChannelChange = this.onChannelChange.bind(this);
+ this.onChannelLeave = this.onChannelLeave.bind(this);
+ this.onPostsChange = this.onPostsChange.bind(this);
+ this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this);
+ this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
+ this.postsLoaded = this.postsLoaded.bind(this);
+ this.postsLoadedFailure = this.postsLoadedFailure.bind(this);
+ this.handlePostsViewJumpRequest = this.handlePostsViewJumpRequest.bind(this);
+
+ const currentChannelId = ChannelStore.getCurrentId();
+ const state = {
+ scrollType: PostsView.SCROLL_TYPE_BOTTOM,
+ scrollPost: null,
+ numPostsToDisplay: Constants.POST_CHUNK_SIZE
+ };
+ if (currentChannelId) {
+ Object.assign(state, {
+ currentChannelIndex: 0,
+ channels: [currentChannelId],
+ postLists: [this.getChannelPosts(currentChannelId)]
+ });
+ } else {
+ Object.assign(state, {
+ currentChannelIndex: null,
+ channels: [],
+ postLists: []
+ });
+ }
+
+ this.state = state;
+ }
+ componentDidMount() {
+ ChannelStore.addChangeListener(this.onChannelChange);
+ ChannelStore.addLeaveListener(this.onChannelLeave);
+ PostStore.addChangeListener(this.onPostsChange);
+ PostStore.addPostsViewJumpListener(this.handlePostsViewJumpRequest);
+ }
+ componentWillUnmount() {
+ ChannelStore.removeChangeListener(this.onChannelChange);
+ ChannelStore.removeLeaveListener(this.onChannelLeave);
+ PostStore.removeChangeListener(this.onPostsChange);
+ PostStore.removePostsViewJumpListener(this.handlePostsViewJumpRequest);
+ }
+ handlePostsViewJumpRequest(type, post) {
+ switch (type) {
+ case Constants.PostsViewJumpTypes.BOTTOM:
+ this.setState({scrollType: PostsView.SCROLL_TYPE_BOTTOM});
+ break;
+ case Constants.PostsViewJumpTypes.POST:
+ this.setState({
+ scrollType: PostsView.SCROLL_TYPE_POST,
+ scrollPost: post
+ });
+ break;
+ case Constants.PostsViewJumpTypes.SIDEBAR_OPEN:
+ this.setState({scrollType: PostsView.SIDEBAR_OPEN});
+ break;
+ }
+ }
+ onChannelChange() {
+ const postLists = Object.assign({}, this.state.postLists);
+ const channels = this.state.channels.slice();
+ const channelId = ChannelStore.getCurrentId();
+
+ // Has the channel really changed?
+ if (channelId === channels[this.state.currentChannelIndex]) {
+ return;
+ }
+
+ PostStore.clearUnseenDeletedPosts(channelId);
+
+ let lastViewed = Number.MAX_VALUE;
+ const member = ChannelStore.getMember(channelId);
+ if (member != null) {
+ lastViewed = member.last_viewed_at;
+ }
+
+ let newIndex = channels.indexOf(channelId);
+ if (newIndex === -1) {
+ newIndex = channels.length;
+ channels.push(channelId);
+ postLists[newIndex] = this.getChannelPosts(channelId);
+ }
+ this.setState({
+ currentChannelIndex: newIndex,
+ currentLastViewed: lastViewed,
+ scrollType: PostsView.SCROLL_TYPE_BOTTOM,
+ channels,
+ postLists});
+ }
+ onChannelLeave(id) {
+ const postLists = Object.assign({}, this.state.postLists);
+ const channels = this.state.channels.slice();
+ const index = channels.indexOf(id);
+ if (index !== -1) {
+ postLists.splice(index, 1);
+ channels.splice(index, 1);
+ }
+ this.setState({channels, postLists});
+ }
+ onPostsChange() {
+ const channels = this.state.channels;
+ const postLists = Object.assign({}, this.state.postLists);
+ const newPostsView = this.getChannelPosts(channels[this.state.currentChannelIndex]);
+
+ postLists[this.state.currentChannelIndex] = newPostsView;
+ this.setState({postLists});
+ }
+ getChannelPosts(id) {
+ const postList = PostStore.getPosts(id);
+
+ if (postList != null) {
+ const deletedPosts = PostStore.getUnseenDeletedPosts(id);
+
+ if (deletedPosts && Object.keys(deletedPosts).length > 0) {
+ for (const pid in deletedPosts) {
+ if (deletedPosts.hasOwnProperty(pid)) {
+ postList.posts[pid] = deletedPosts[pid];
+ postList.order.unshift(pid);
+ }
+ }
+
+ postList.order.sort((a, b) => {
+ if (postList.posts[a].create_at > postList.posts[b].create_at) {
+ return -1;
+ }
+ if (postList.posts[a].create_at < postList.posts[b].create_at) {
+ return 1;
+ }
+ return 0;
+ });
+ }
+
+ const pendingPostList = PostStore.getPendingPosts(id);
+
+ if (pendingPostList) {
+ postList.order = pendingPostList.order.concat(postList.order);
+ for (const ppid in pendingPostList.posts) {
+ if (pendingPostList.posts.hasOwnProperty(ppid)) {
+ postList.posts[ppid] = pendingPostList.posts[ppid];
+ }
+ }
+ }
+ }
+
+ return postList;
+ }
+ loadMorePostsTop() {
+ const postLists = this.state.postLists;
+ const channels = this.state.channels;
+ const currentChannelId = channels[this.state.currentChannelIndex];
+ const currentPostList = postLists[this.state.currentChannelIndex];
+
+ this.setState({numPostsToDisplay: this.state.numPostsToDisplay + Constants.POST_CHUNK_SIZE});
+
+ Client.getPostsPage(
+ currentChannelId,
+ currentPostList.order.length,
+ Constants.POST_CHUNK_SIZE,
+ this.postsLoaded,
+ this.postsLoadedFailure
+ );
+ }
+ postsLoaded(data) {
+ if (!data) {
+ return;
+ }
+
+ if (data.order.length === 0) {
+ return;
+ }
+
+ const postLists = this.state.postLists;
+ const currentPostList = postLists[this.state.currentChannelIndex];
+ const channels = this.state.channels;
+ const currentChannelId = channels[this.state.currentChannelIndex];
+
+ var newPostList = {};
+ newPostList.posts = Object.assign(currentPostList.posts, data.posts);
+ newPostList.order = currentPostList.order.concat(data.order);
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_POSTS,
+ id: currentChannelId,
+ post_list: newPostList
+ });
+
+ Client.getProfiles();
+ }
+ postsLoadedFailure(err) {
+ AsyncClient.dispatchError(err, 'getPosts');
+ }
+ handlePostsViewScroll(atBottom) {
+ if (atBottom) {
+ this.setState({scrollType: PostsView.SCROLL_TYPE_BOTTOM});
+ } else {
+ this.setState({scrollType: PostsView.SCROLL_TYPE_FREE});
+ }
+ }
+ shouldComponentUpdate(nextProps, nextState) {
+ if (Utils.areStatesEqual(this.state, nextState)) {
+ return false;
+ }
+
+ return true;
+ }
+ render() {
+ const postLists = this.state.postLists;
+ const channels = this.state.channels;
+ const currentChannelId = channels[this.state.currentChannelIndex];
+ const channel = ChannelStore.get(currentChannelId);
+
+ const postListCtls = [];
+ for (let i = 0; i < channels.length; i++) {
+ const isActive = (channels[i] === currentChannelId);
+ postListCtls.push(
+ <PostsView
+ key={'postsviewkey' + i}
+ isActive={isActive}
+ postList={postLists[i]}
+ scrollType={this.state.scrollType}
+ scrollPost={this.state.scrollPost}
+ postViewScrolled={this.handlePostsViewScroll}
+ loadMorePostsTopClicked={this.loadMorePostsTop}
+ numPostsToDisplay={this.state.numPostsToDisplay}
+ introText={channel ? createChannelIntroMessage(channel) : null}
+ messageSeparatorTime={this.state.currentLastViewed}
+ />
+ );
+ if ((!postLists[i] || !channel) && isActive) {
+ postListCtls.push(
+ <LoadingScreen
+ position='absolute'
+ key='loading'
+ />
+ );
+ }
+ }
+
+ return (
+ <div>{postListCtls}</div>
+ );
+ }
+}
diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx
index bcdec2870..fe57bed28 100644
--- a/web/react/components/rhs_thread.jsx
+++ b/web/react/components/rhs_thread.jsx
@@ -34,12 +34,12 @@ export default class RhsThread extends React.Component {
}
var channelId = postList.posts[postList.order[0]].channel_id;
- var pendingPostList = PostStore.getPendingPosts(channelId);
+ var pendingPostsList = PostStore.getPendingPosts(channelId);
- if (pendingPostList) {
- for (var pid in pendingPostList.posts) {
- if (pendingPostList.posts.hasOwnProperty(pid)) {
- postList.posts[pid] = pendingPostList.posts[pid];
+ if (pendingPostsList) {
+ for (var pid in pendingPostsList.posts) {
+ if (pendingPostsList.posts.hasOwnProperty(pid)) {
+ postList.posts[pid] = pendingPostsList.posts[pid];
}
}
}
diff --git a/web/react/components/search_autocomplete.jsx b/web/react/components/search_autocomplete.jsx
index f7d772677..03e14ec49 100644
--- a/web/react/components/search_autocomplete.jsx
+++ b/web/react/components/search_autocomplete.jsx
@@ -142,7 +142,10 @@ export default class SearchAutocomplete extends React.Component {
let channels = ChannelStore.getAll();
if (filter) {
- channels = channels.filter((channel) => channel.name.startsWith(filter));
+ channels = channels.filter((channel) => channel.name.startsWith(filter) && channel.type !== 'D');
+ } else {
+ // don't show direct channels
+ channels = channels.filter((channel) => channel.type !== 'D');
}
channels.sort((a, b) => a.name.localeCompare(b.name));
diff --git a/web/react/components/search_bar.jsx b/web/react/components/search_bar.jsx
index 83c10494a..90865475b 100644
--- a/web/react/components/search_bar.jsx
+++ b/web/react/components/search_bar.jsx
@@ -90,14 +90,10 @@ export default class SearchBar extends React.Component {
this.refs.autocomplete.handleInputChange(e.target, term);
}
- handleMouseInput(e) {
- e.preventDefault();
- }
handleUserBlur() {
this.setState({focused: false});
}
- handleUserFocus(e) {
- e.target.select();
+ handleUserFocus() {
$('.search-bar__container').addClass('focused');
this.setState({focused: true});
@@ -106,14 +102,8 @@ export default class SearchBar extends React.Component {
if (terms.length) {
this.setState({isSearching: true});
- // append * if not present
- let searchTerms = terms;
- if (searchTerms.search(/\*\s*$/) === -1) {
- searchTerms = searchTerms + '*';
- }
-
client.search(
- searchTerms,
+ terms,
(data) => {
this.setState({isSearching: false});
if (utils.isMobile()) {
@@ -185,6 +175,7 @@ export default class SearchBar extends React.Component {
className='search__form relative-div'
onSubmit={this.handleSubmit}
style={{overflow: 'visible'}}
+ autoComplete='off'
>
<span className='glyphicon glyphicon-search sidebar__search-icon' />
<input
@@ -197,7 +188,6 @@ export default class SearchBar extends React.Component {
onBlur={this.handleUserBlur}
onChange={this.handleUserInput}
onKeyDown={this.handleKeyDown}
- onMouseUp={this.handleMouseInput}
/>
{isSearching}
<SearchAutocomplete
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index 5cb6d168b..023955e97 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -136,7 +136,7 @@ export default class Sidebar extends React.Component {
channel.type = 'D';
}
- channel.display_name = teammate.username;
+ channel.display_name = Utils.displayUsername(teammate.id);
channel.teammate_id = teammate.id;
channel.status = UserStore.getStatus(teammate.id);
@@ -178,10 +178,6 @@ export default class Sidebar extends React.Component {
window.addEventListener('resize', this.handleResize);
}
shouldComponentUpdate(nextProps, nextState) {
- if (!Utils.areStatesEqual(nextProps, this.props)) {
- return true;
- }
-
if (!Utils.areStatesEqual(nextState, this.state)) {
return true;
}
@@ -235,7 +231,7 @@ export default class Sidebar extends React.Component {
const unread = this.getUnreadCount();
const mentionTitle = unread.mentions > 0 ? '(' + unread.mentions + ') ' : '';
const unreadTitle = unread.msgs > 0 ? '* ' : '';
- document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + this.props.teamDisplayName + ' ' + currentSiteName;
+ document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + TeamStore.getCurrent().display_name + ' ' + currentSiteName;
}
}
onScroll() {
@@ -543,9 +539,9 @@ export default class Sidebar extends React.Component {
/>
<SidebarHeader
- teamDisplayName={this.props.teamDisplayName}
- teamName={this.props.teamName}
- teamType={this.props.teamType}
+ teamDisplayName={TeamStore.getCurrent().display_name}
+ teamName={TeamStore.getCurrent().name}
+ teamType={TeamStore.getCurrent().type}
/>
<SearchBox />
@@ -631,11 +627,6 @@ export default class Sidebar extends React.Component {
}
Sidebar.defaultProps = {
- teamType: '',
- teamDisplayName: ''
};
Sidebar.propTypes = {
- teamType: React.PropTypes.string,
- teamDisplayName: React.PropTypes.string,
- teamName: React.PropTypes.string
};
diff --git a/web/react/components/sidebar_right.jsx b/web/react/components/sidebar_right.jsx
index 51225cbbe..e2ef60959 100644
--- a/web/react/components/sidebar_right.jsx
+++ b/web/react/components/sidebar_right.jsx
@@ -20,23 +20,48 @@ export default class SidebarRight extends React.Component {
this.onSelectedChange = this.onSelectedChange.bind(this);
this.onSearchChange = this.onSearchChange.bind(this);
+ this.doStrangeThings = this.doStrangeThings.bind(this);
+
this.state = getStateFromStores();
}
componentDidMount() {
SearchStore.addSearchChangeListener(this.onSearchChange);
PostStore.addSelectedPostChangeListener(this.onSelectedChange);
+ this.doStrangeThings();
}
componentWillUnmount() {
SearchStore.removeSearchChangeListener(this.onSearchChange);
PostStore.removeSelectedPostChangeListener(this.onSelectedChange);
}
- componentDidUpdate() {
- if (this.plScrolledToBottom) {
- var postHolder = $('.post-list-holder-by-time').not('.inactive');
- postHolder.scrollTop(postHolder[0].scrollHeight);
- } else {
- $('.top-visible-post')[0].scrollIntoView();
+ componentWillUpdate() {
+ PostStore.jumpPostsViewSidebarOpen();
+ }
+ doStrangeThings() {
+ // We should have a better way to do this stuff
+ // Hence the function name.
+ $('.inner__wrap').removeClass('.move--right');
+ $('.inner__wrap').addClass('move--left');
+ $('.sidebar--left').removeClass('move--right');
+ $('.sidebar--right').addClass('move--left');
+
+ //$('.sidebar--right').prepend('<div class="sidebar__overlay"></div>');
+
+ if (!(this.state.search_visible || this.state.post_right_visible)) {
+ $('.inner__wrap').removeClass('move--left').removeClass('move--right');
+ $('.sidebar--right').removeClass('move--left');
+ return (
+ <div></div>
+ );
}
+
+ /*setTimeout(() => {
+ $('.sidebar__overlay').fadeOut('200', () => {
+ $('.sidebar__overlay').remove();
+ });
+ }, 500);*/
+ }
+ componentDidUpdate() {
+ this.doStrangeThings();
}
onSelectedChange(fromSearch) {
var newState = getStateFromStores(fromSearch);
@@ -52,30 +77,6 @@ export default class SidebarRight extends React.Component {
}
}
render() {
- var postHolder = $('.post-list-holder-by-time').not('.inactive');
- const position = postHolder.scrollTop() + postHolder.height() + 14;
- const bottom = postHolder[0].scrollHeight;
- this.plScrolledToBottom = position >= bottom;
-
- if (!(this.state.search_visible || this.state.post_right_visible)) {
- $('.inner__wrap').removeClass('move--left').removeClass('move--right');
- $('.sidebar--right').removeClass('move--left');
- return (
- <div></div>
- );
- }
-
- $('.inner__wrap').removeClass('.move--right').addClass('move--left');
- $('.sidebar--left').removeClass('move--right');
- $('.sidebar--right').addClass('move--left');
- $('.sidebar--right').prepend('<div class="sidebar__overlay"></div>');
-
- setTimeout(() => {
- $('.sidebar__overlay').fadeOut('200', function fadeOverlay() {
- $(this).remove();
- });
- }, 500);
-
var content = '';
if (this.state.search_visible) {
diff --git a/web/react/components/team_signup_display_name_page.jsx b/web/react/components/team_signup_display_name_page.jsx
index 4d08274e4..2005ecc31 100644
--- a/web/react/components/team_signup_display_name_page.jsx
+++ b/web/react/components/team_signup_display_name_page.jsx
@@ -25,6 +25,9 @@ export default class TeamSignupDisplayNamePage extends React.Component {
if (!displayName) {
this.setState({nameError: 'This field is required'});
return;
+ } else if (displayName.length < 4 || displayName.length > 15) {
+ this.setState({nameError: 'Name must be 4 or more characters up to a maximum of 15'});
+ return;
}
this.props.state.wizard = 'team_url';
diff --git a/web/react/components/team_signup_url_page.jsx b/web/react/components/team_signup_url_page.jsx
index 02d5cab8e..8972fda1a 100644
--- a/web/react/components/team_signup_url_page.jsx
+++ b/web/react/components/team_signup_url_page.jsx
@@ -35,8 +35,8 @@ export default class TeamSignupUrlPage extends React.Component {
if (cleanedName !== name || !urlRegex.test(name)) {
this.setState({nameError: "Use only lower case letters, numbers and dashes. Must start with a letter and can't end in a dash."});
return;
- } else if (cleanedName.length <= 2 || cleanedName.length > 15) {
- this.setState({nameError: 'Name must be 3 or more characters up to a maximum of 15'});
+ } else if (cleanedName.length < 4 || cleanedName.length > 15) {
+ this.setState({nameError: 'Name must be 4 or more characters up to a maximum of 15'});
return;
}
diff --git a/web/react/components/time_since.jsx b/web/react/components/time_since.jsx
new file mode 100644
index 000000000..c37739b9c
--- /dev/null
+++ b/web/react/components/time_since.jsx
@@ -0,0 +1,50 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Utils = require('../utils/utils.jsx');
+
+var Tooltip = ReactBootstrap.Tooltip;
+var OverlayTrigger = ReactBootstrap.OverlayTrigger;
+
+export default class TimeSince extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+ componentDidMount() {
+ this.intervalId = setInterval(() => {
+ this.forceUpdate();
+ }, 30000);
+ }
+ componentWillUnmount() {
+ clearInterval(this.intervalId);
+ }
+ render() {
+ const displayDate = Utils.displayDate(this.props.eventTime);
+ const displayTime = Utils.displayTime(this.props.eventTime);
+
+ const tooltip = (
+ <Tooltip id={'time-since-tooltip-' + this.props.eventTime}>
+ {displayDate + ' at ' + displayTime}
+ </Tooltip>
+ );
+
+ return (
+ <OverlayTrigger
+ delayShow={400}
+ placement='top'
+ overlay={tooltip}
+ >
+ <time className='post-profile-time'>
+ {Utils.displayDateTime(this.props.eventTime)}
+ </time>
+ </OverlayTrigger>
+ );
+ }
+}
+TimeSince.defaultProps = {
+ eventTime: 0
+};
+
+TimeSince.propTypes = {
+ eventTime: React.PropTypes.number.isRequired
+};
diff --git a/web/react/components/user_settings/manage_outgoing_hooks.jsx b/web/react/components/user_settings/manage_outgoing_hooks.jsx
index 4c56db0a1..93be988d1 100644
--- a/web/react/components/user_settings/manage_outgoing_hooks.jsx
+++ b/web/react/components/user_settings/manage_outgoing_hooks.jsx
@@ -1,11 +1,12 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var Client = require('../../utils/client.jsx');
-var Constants = require('../../utils/constants.jsx');
-var ChannelStore = require('../../stores/channel_store.jsx');
-var LoadingScreen = require('../loading_screen.jsx');
+const LoadingScreen = require('../loading_screen.jsx');
+const ChannelStore = require('../../stores/channel_store.jsx');
+
+const Client = require('../../utils/client.jsx');
+const Constants = require('../../utils/constants.jsx');
export default class ManageOutgoingHooks extends React.Component {
constructor() {
@@ -45,10 +46,10 @@ export default class ManageOutgoingHooks extends React.Component {
hooks = [];
}
hooks.push(data);
- this.setState({hooks, serverError: null, channelId: '', triggerWords: '', callbackURLs: ''});
+ this.setState({hooks, addError: null, channelId: '', triggerWords: '', callbackURLs: ''});
},
(err) => {
- this.setState({serverError: err});
+ this.setState({addError: err.message});
}
);
}
@@ -75,7 +76,7 @@ export default class ManageOutgoingHooks extends React.Component {
this.setState({hooks});
},
(err) => {
- this.setState({serverError: err});
+ this.setState({editError: err.message});
}
);
}
@@ -94,10 +95,10 @@ export default class ManageOutgoingHooks extends React.Component {
}
}
- this.setState({hooks, serverError: null});
+ this.setState({hooks, editError: null});
},
(err) => {
- this.setState({serverError: err});
+ this.setState({editError: err.message});
}
);
}
@@ -105,11 +106,11 @@ export default class ManageOutgoingHooks extends React.Component {
Client.listOutgoingHooks(
(data) => {
if (data) {
- this.setState({hooks: data, getHooksComplete: true, serverError: null});
+ this.setState({hooks: data, getHooksComplete: true, editError: null});
}
},
(err) => {
- this.setState({serverError: err});
+ this.setState({editError: err.message});
}
);
}
@@ -123,9 +124,13 @@ export default class ManageOutgoingHooks extends React.Component {
this.setState({callbackURLs: e.target.value});
}
render() {
- let serverError;
- if (this.state.serverError) {
- serverError = <label className='has-error'>{this.state.serverError}</label>;
+ let addError;
+ if (this.state.addError) {
+ addError = <label className='has-error'>{this.state.addError}</label>;
+ }
+ let editError;
+ if (this.state.editError) {
+ addError = <label className='has-error'>{this.state.editError}</label>;
}
const channels = ChannelStore.getAll();
@@ -235,6 +240,7 @@ export default class ManageOutgoingHooks extends React.Component {
return (
<div key='addOutgoingHook'>
+ {'Create webhooks to send new message events to an external integration. Please see '}<a href='http://mattermost.org/webhooks'>{'http://mattermost.org/webhooks'}</a> {' to learn more.'}
<label className='control-label'>{'Add a new outgoing webhook'}</label>
<div className='padding-top divider-light'></div>
<div className='padding-top'>
@@ -275,10 +281,11 @@ export default class ManageOutgoingHooks extends React.Component {
resize={false}
rows={3}
onChange={this.updateCallbackURLs}
+ placeholder='Each URL must start with http:// or https://'
/>
</div>
<div className='padding-top'>{'New line separated URLs that will receive the HTTP POST event'}</div>
- {serverError}
+ {addError}
</div>
<div className='padding-top padding-bottom'>
<a
@@ -292,6 +299,7 @@ export default class ManageOutgoingHooks extends React.Component {
</div>
</div>
{existingHooks}
+ {editError}
</div>
);
}
diff --git a/web/react/components/user_settings/user_settings_display.jsx b/web/react/components/user_settings/user_settings_display.jsx
index 22a62273c..d086c78a9 100644
--- a/web/react/components/user_settings/user_settings_display.jsx
+++ b/web/react/components/user_settings/user_settings_display.jsx
@@ -9,8 +9,12 @@ import PreferenceStore from '../../stores/preference_store.jsx';
function getDisplayStateFromStores() {
const militaryTime = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', {value: 'false'});
+ const nameFormat = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'username'});
- return {militaryTime: militaryTime.value};
+ return {
+ militaryTime: militaryTime.value,
+ nameFormat: nameFormat.value
+ };
}
export default class UserSettingsDisplay extends React.Component {
@@ -19,15 +23,17 @@ export default class UserSettingsDisplay extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.handleClockRadio = this.handleClockRadio.bind(this);
+ this.handleNameRadio = this.handleNameRadio.bind(this);
this.updateSection = this.updateSection.bind(this);
this.handleClose = this.handleClose.bind(this);
this.state = getDisplayStateFromStores();
}
handleSubmit() {
- const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', this.state.militaryTime);
+ const timePreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', this.state.militaryTime);
+ const namePreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', this.state.nameFormat);
- savePreferences([preference],
+ savePreferences([timePreference, namePreference],
() => {
PreferenceStore.emitChange();
this.updateSection('');
@@ -40,6 +46,9 @@ export default class UserSettingsDisplay extends React.Component {
handleClockRadio(militaryTime) {
this.setState({militaryTime});
}
+ handleNameRadio(nameFormat) {
+ this.setState({nameFormat});
+ }
updateSection(section) {
this.setState(getDisplayStateFromStores());
this.props.updateSection(section);
@@ -56,6 +65,7 @@ export default class UserSettingsDisplay extends React.Component {
render() {
const serverError = this.state.serverError || null;
let clockSection;
+ let nameFormatSection;
if (this.props.activeSection === 'clock') {
const clockFormat = [false, false];
if (this.state.militaryTime === 'true') {
@@ -127,6 +137,88 @@ export default class UserSettingsDisplay extends React.Component {
);
}
+ if (this.props.activeSection === 'name_format') {
+ const nameFormat = [false, false, false];
+ if (this.state.nameFormat === 'nickname_full_name') {
+ nameFormat[0] = true;
+ } else if (this.state.nameFormat === 'full_name') {
+ nameFormat[2] = true;
+ } else {
+ nameFormat[1] = true;
+ }
+
+ const inputs = [
+ <div key='userDisplayNameOptions'>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ checked={nameFormat[0]}
+ onChange={this.handleNameRadio.bind(this, 'nickname_full_name')}
+ />
+ {'Show nickname if one exists, otherwise show first and last name (team default)'}
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ checked={nameFormat[1]}
+ onChange={this.handleNameRadio.bind(this, 'username')}
+ />
+ {'Show username'}
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ checked={nameFormat[2]}
+ onChange={this.handleNameRadio.bind(this, 'full_name')}
+ />
+ {'Show first and last name'}
+ </label>
+ <br/>
+ </div>
+ <div><br/>{'How should other users be shown in Direct Messages list?'}</div>
+ </div>
+ ];
+
+ nameFormatSection = (
+ <SettingItemMax
+ title='Show real names, nick names or usernames?'
+ inputs={inputs}
+ submit={this.handleSubmit}
+ server_error={serverError}
+ updateSection={(e) => {
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ let describe = '';
+ if (this.state.nameFormat === 'username') {
+ describe = 'Show username';
+ } else if (this.state.nameFormat === 'full_name') {
+ describe = 'Show first and last name';
+ } else {
+ describe = 'Show nickname if one exists, otherwise show first and last name (team default)';
+ }
+
+ nameFormatSection = (
+ <SettingItemMin
+ title='Show real names, nick names or usernames?'
+ describe={describe}
+ updateSection={() => {
+ this.props.updateSection('name_format');
+ }}
+ />
+ );
+ }
+
return (
<div>
<div className='modal-header'>
@@ -151,6 +243,8 @@ export default class UserSettingsDisplay extends React.Component {
<div className='divider-dark first'/>
{clockSection}
<div className='divider-dark'/>
+ {nameFormatSection}
+ <div className='divider-dark'/>
</div>
</div>
);
diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx
index 9bee74343..4a9915a1f 100644
--- a/web/react/components/user_settings/user_settings_integrations.jsx
+++ b/web/react/components/user_settings/user_settings_integrations.jsx
@@ -56,7 +56,7 @@ export default class UserSettingsIntegrationsTab extends React.Component {
<SettingItemMin
title='Incoming Webhooks'
width='medium'
- describe='Manage your incoming webhooks (Developer feature)'
+ describe='Manage your incoming webhooks'
updateSection={() => {
this.updateSection('incoming-hooks');
}}
diff --git a/web/react/components/user_settings/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx
index 61d49acb2..2b904763c 100644
--- a/web/react/components/user_settings/user_settings_notifications.jsx
+++ b/web/react/components/user_settings/user_settings_notifications.jsx
@@ -37,18 +37,18 @@ function getNotificationsStateFromStores() {
if (user.notify_props.mention_keys) {
var keys = user.notify_props.mention_keys.split(',');
- if (keys.indexOf(user.username) !== -1) {
+ if (keys.indexOf(user.username) === -1) {
+ usernameKey = false;
+ } else {
usernameKey = true;
keys.splice(keys.indexOf(user.username), 1);
- } else {
- usernameKey = false;
}
- if (keys.indexOf('@' + user.username) !== -1) {
+ if (keys.indexOf('@' + user.username) === -1) {
+ mentionKey = false;
+ } else {
mentionKey = true;
keys.splice(keys.indexOf('@' + user.username), 1);
- } else {
- mentionKey = false;
}
customKeys = keys.join(',');
diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx
index 7a04c5979..067dcde50 100644
--- a/web/react/pages/channel.jsx
+++ b/web/react/pages/channel.jsx
@@ -2,13 +2,12 @@
// See License.txt for license information.
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-var Navbar = require('../components/navbar.jsx');
-var Sidebar = require('../components/sidebar.jsx');
-var ChannelHeader = require('../components/channel_header.jsx');
-var PostListContainer = require('../components/post_list_container.jsx');
-var CreatePost = require('../components/create_post.jsx');
-var SidebarRight = require('../components/sidebar_right.jsx');
-var SidebarRightMenu = require('../components/sidebar_right_menu.jsx');
+var ChannelView = require('../components/channel_view.jsx');
+var ChannelLoader = require('../components/channel_loader.jsx');
+var ErrorBar = require('../components/error_bar.jsx');
+var ErrorStore = require('../stores/error_store.jsx');
+
+var MentionList = require('../components/mention_list.jsx');
var GetLinkModal = require('../components/get_link_modal.jsx');
var MemberInviteModal = require('../components/invite_member_modal.jsx');
var EditChannelModal = require('../components/edit_channel_modal.jsx');
@@ -24,15 +23,10 @@ var TeamSettingsModal = require('../components/team_settings_modal.jsx');
var ChannelMembersModal = require('../components/channel_members.jsx');
var ChannelInviteModal = require('../components/channel_invite_modal.jsx');
var TeamMembersModal = require('../components/team_members.jsx');
-var ErrorBar = require('../components/error_bar.jsx');
-var ErrorStore = require('../stores/error_store.jsx');
-var ChannelLoader = require('../components/channel_loader.jsx');
-var MentionList = require('../components/mention_list.jsx');
var ChannelInfoModal = require('../components/channel_info_modal.jsx');
var AccessHistoryModal = require('../components/access_history_modal.jsx');
var ActivityLogModal = require('../components/activity_log_modal.jsx');
var RemovedFromChannelModal = require('../components/removed_from_channel_modal.jsx');
-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');
@@ -61,20 +55,29 @@ function setupChannelPage(props) {
);
ReactDOM.render(
- <Navbar teamDisplayName={props.TeamDisplayName} />,
- document.getElementById('navbar')
+ <ChannelView/>,
+ document.getElementById('channel_view')
);
ReactDOM.render(
- <Sidebar
- teamDisplayName={props.TeamDisplayName}
- teamName={props.TeamName}
- teamType={props.TeamType}
- />,
- document.getElementById('sidebar-left')
+ <MentionList id='post_textbox' />,
+ document.getElementById('post_mention_tab')
);
ReactDOM.render(
+ <MentionList id='reply_textbox' />,
+ document.getElementById('reply_mention_tab')
+ );
+
+ ReactDOM.render(
+ <MentionList id='edit_textbox' />,
+ document.getElementById('edit_mention_tab')
+ );
+
+ //
+ // Modals
+ //
+ ReactDOM.render(
<GetLinkModal />,
document.getElementById('get_link_modal')
);
@@ -105,11 +108,6 @@ function setupChannelPage(props) {
);
ReactDOM.render(
- <ChannelHeader />,
- document.getElementById('channel-header')
- );
-
- ReactDOM.render(
<EditChannelModal />,
document.getElementById('edit_channel_modal')
);
@@ -150,11 +148,6 @@ function setupChannelPage(props) {
);
ReactDOM.render(
- <PostListContainer />,
- document.getElementById('post-list')
- );
-
- ReactDOM.render(
<EditPostModal />,
document.getElementById('edit_post_modal')
);
@@ -170,39 +163,6 @@ function setupChannelPage(props) {
);
ReactDOM.render(
- <CreatePost />,
- document.getElementById('post-create')
- );
-
- ReactDOM.render(
- <SidebarRight />,
- document.getElementById('sidebar-right')
- );
-
- ReactDOM.render(
- <SidebarRightMenu
- teamDisplayName={props.TeamDisplayName}
- teamType={props.TeamType}
- />,
- document.getElementById('sidebar-menu')
- );
-
- ReactDOM.render(
- <MentionList id='post_textbox' />,
- document.getElementById('post_mention_tab')
- );
-
- ReactDOM.render(
- <MentionList id='reply_textbox' />,
- document.getElementById('reply_mention_tab')
- );
-
- ReactDOM.render(
- <MentionList id='edit_textbox' />,
- document.getElementById('edit_mention_tab')
- );
-
- ReactDOM.render(
<AccessHistoryModal />,
document.getElementById('access_history_modal')
);
@@ -218,13 +178,6 @@ function setupChannelPage(props) {
);
ReactDOM.render(
- <FileUploadOverlay
- overlayType='center'
- />,
- document.getElementById('file_upload_overlay')
- );
-
- ReactDOM.render(
<RegisterAppModal />,
document.getElementById('register_app_modal')
);
diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx
index 8f4e30e7c..0fe253310 100644
--- a/web/react/stores/post_store.jsx
+++ b/web/react/stores/post_store.jsx
@@ -14,6 +14,7 @@ var ActionTypes = Constants.ActionTypes;
var CHANGE_EVENT = 'change';
var SELECTED_POST_CHANGE_EVENT = 'selected_post_change';
var EDIT_POST_EVENT = 'edit_post';
+var POSTS_VIEW_JUMP_EVENT = 'post_list_jump';
class PostStoreClass extends EventEmitter {
constructor() {
@@ -29,7 +30,11 @@ class PostStoreClass extends EventEmitter {
this.emitEditPost = this.emitEditPost.bind(this);
this.addEditPostListener = this.addEditPostListener.bind(this);
- this.removeEditPostListener = this.removeEditPostListener.bind(this);
+ this.removeEditPostListener = this.removeEditPostListner.bind(this);
+
+ this.emitPostsViewJump = this.emitPostsViewJump.bind(this);
+ this.addPostsViewJumpListener = this.addPostsViewJumpListener.bind(this);
+ this.removePostsViewJumpListener = this.removePostsViewJumpListener.bind(this);
this.getCurrentPosts = this.getCurrentPosts.bind(this);
this.storePosts = this.storePosts.bind(this);
@@ -96,10 +101,34 @@ class PostStoreClass extends EventEmitter {
this.on(EDIT_POST_EVENT, callback);
}
- removeEditPostListener(callback) {
+ removeEditPostListner(callback) {
this.removeListener(EDIT_POST_EVENT, callback);
}
+ emitPostsViewJump(type, post) {
+ this.emit(POSTS_VIEW_JUMP_EVENT, type, post);
+ }
+
+ addPostsViewJumpListener(callback) {
+ this.on(POSTS_VIEW_JUMP_EVENT, callback);
+ }
+
+ removePostsViewJumpListener(callback) {
+ this.removeListener(POSTS_VIEW_JUMP_EVENT, callback);
+ }
+
+ jumpPostsViewToBottom() {
+ this.emitPostsViewJump(Constants.PostsViewJumpTypes.BOTTOM, null);
+ }
+
+ jumpPostsViewToPost(post) {
+ this.emitPostsViewJump(Constants.PostsViewJumpTypes.POST, post);
+ }
+
+ jumpPostsViewSidebarOpen() {
+ this.emitPostsViewJump(Constants.PostsViewJumpTypes.SIDEBAR_OPEN, null);
+ }
+
getCurrentPosts() {
var currentId = ChannelStore.getCurrentId();
@@ -108,16 +137,16 @@ class PostStoreClass extends EventEmitter {
}
return null;
}
- storePosts(channelId, newPostList) {
- if (isPostListNull(newPostList)) {
+ storePosts(channelId, newPostsView) {
+ if (isPostListNull(newPostsView)) {
return;
}
var postList = makePostListNonNull(this.getPosts(channelId));
- for (const pid in newPostList.posts) {
- if (newPostList.posts.hasOwnProperty(pid)) {
- const np = newPostList.posts[pid];
+ for (const pid in newPostsView.posts) {
+ if (newPostsView.posts.hasOwnProperty(pid)) {
+ const np = newPostsView.posts[pid];
if (np.delete_at === 0) {
postList.posts[pid] = np;
if (postList.order.indexOf(pid) === -1) {
diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx
index 4d69a6716..4efeb7c8f 100644
--- a/web/react/stores/socket_store.jsx
+++ b/web/react/stores/socket_store.jsx
@@ -165,7 +165,7 @@ function handleNewPostEvent(msg) {
}
// Send desktop notification
- if (UserStore.getCurrentId() !== msg.user_id) {
+ if (UserStore.getCurrentId() !== msg.user_id || post.props.from_webhook === 'true') {
const msgProps = msg.props;
let mentions = [];
@@ -189,7 +189,9 @@ function handleNewPostEvent(msg) {
}
let username = 'Someone';
- if (UserStore.hasProfile(msg.user_id)) {
+ if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') {
+ username = post.props.override_username;
+ } else if (UserStore.hasProfile(msg.user_id)) {
username = UserStore.getProfile(msg.user_id).username;
}
diff --git a/web/react/utils/channel_intro_mssages.jsx b/web/react/utils/channel_intro_mssages.jsx
new file mode 100644
index 000000000..b3f868456
--- /dev/null
+++ b/web/react/utils/channel_intro_mssages.jsx
@@ -0,0 +1,218 @@
+
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const Utils = require('./utils.jsx');
+const UserProfile = require('../components/user_profile.jsx');
+const ChannelStore = require('../stores/channel_store.jsx');
+const Constants = require('../utils/constants.jsx');
+const TeamStore = require('../stores/team_store.jsx');
+
+export function createChannelIntroMessage(channel) {
+ if (channel.type === 'D') {
+ return createDMIntroMessage(channel);
+ } else if (ChannelStore.isDefault(channel)) {
+ return createDefaultIntroMessage(channel);
+ } else if (channel.name === Constants.OFFTOPIC_CHANNEL) {
+ return createOffTopicIntroMessage(channel);
+ } else if (channel.type === 'O' || channel.type === 'P') {
+ return createStandardIntroMessage(channel);
+ }
+}
+
+export function createDMIntroMessage(channel) {
+ var teammate = Utils.getDirectTeammate(channel.id);
+
+ if (teammate) {
+ var teammateName = teammate.username;
+ if (teammate.nickname.length > 0) {
+ teammateName = teammate.nickname;
+ }
+
+ return (
+ <div className='channel-intro'>
+ <div className='post-profile-img__container channel-intro-img'>
+ <img
+ className='post-profile-img'
+ src={'/api/v1/users/' + teammate.id + '/image?time=' + teammate.update_at + '&' + Utils.getSessionIndex()}
+ height='50'
+ width='50'
+ />
+ </div>
+ <div className='channel-intro-profile'>
+ <strong>
+ <UserProfile userId={teammate.id} />
+ </strong>
+ </div>
+ <p className='channel-intro-text'>
+ {'This is the start of your direct message history with ' + teammateName + '.'}<br/>
+ {'Direct messages and files shared here are not shown to people outside this area.'}
+ </p>
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#edit_channel'
+ data-header={channel.header}
+ data-title={channel.display_name}
+ data-channelid={channel.id}
+ >
+ <i className='fa fa-pencil'></i>{'Set a header'}
+ </a>
+ </div>
+ );
+ }
+
+ return (
+ <div className='channel-intro'>
+ <p className='channel-intro-text'>{'This is the start of your direct message history with this teammate. Direct messages and files shared here are not shown to people outside this area.'}</p>
+ </div>
+ );
+}
+
+export function createOffTopicIntroMessage(channel) {
+ return (
+ <div className='channel-intro'>
+ <h4 className='channel-intro__title'>{'Beginning of ' + channel.display_name}</h4>
+ <p className='channel-intro__content'>
+ {'This is the start of ' + channel.display_name + ', a channel for non-work-related conversations.'}
+ <br/>
+ </p>
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#edit_channel'
+ data-header={channel.header}
+ data-title={channel.display_name}
+ data-channelid={channel.id}
+ >
+ <i className='fa fa-pencil'></i>{'Set a header'}
+ </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>
+ );
+}
+
+export function createDefaultIntroMessage(channel) {
+ const team = TeamStore.getCurrent();
+ let inviteModalLink;
+ if (team.type === Constants.INVITE_TEAM) {
+ inviteModalLink = (
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#invite_member'
+ >
+ <i className='fa fa-user-plus'></i>{'Invite others to this team'}
+ </a>
+ );
+ } else {
+ inviteModalLink = (
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#get_link'
+ data-title='Team Invite'
+ data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + team.id}
+ >
+ <i className='fa fa-user-plus'></i>{'Invite others to this team'}
+ </a>
+ );
+ }
+
+ return (
+ <div className='channel-intro'>
+ <h4 className='channel-intro__title'>{'Beginning of ' + channel.display_name}</h4>
+ <p className='channel-intro__content'>
+ <strong>{'Welcome to ' + channel.display_name + '!'}</strong>
+ <br/><br/>
+ {'This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.'}
+ </p>
+ {inviteModalLink}
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#edit_channel'
+ data-header={channel.header}
+ data-title={channel.display_name}
+ data-channelid={channel.id}
+ >
+ <i className='fa fa-pencil'></i>{'Set a header'}
+ </a>
+ <br/>
+ </div>
+ );
+}
+
+export function createStandardIntroMessage(channel) {
+ var uiName = channel.display_name;
+ var creatorName = '';
+
+ var uiType;
+ var memberMessage;
+ if (channel.type === 'P') {
+ uiType = 'private group';
+ memberMessage = ' Only invited members can see this private group.';
+ } else {
+ uiType = 'channel';
+ memberMessage = ' Any member can join and read this channel.';
+ }
+
+ var createMessage;
+ 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 (
+ <div className='channel-intro'>
+ <h4 className='channel-intro__title'>{'Beginning of ' + uiName}</h4>
+ <p className='channel-intro__content'>
+ {createMessage}
+ {memberMessage}
+ <br/>
+ </p>
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#edit_channel'
+ data-header={channel.header}
+ data-title={channel.display_name}
+ data-channelid={channel.id}
+ >
+ <i className='fa fa-pencil'></i>{'Set a header'}
+ </a>
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#channel_invite'
+ >
+ <i className='fa fa-user-plus'></i>{'Invite others to this ' + uiType}
+ </a>
+ </div>
+ );
+}
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 1593f6706..8884d1d10 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -350,5 +350,10 @@ module.exports = {
ruby: 'Ruby',
java: 'Java',
ini: 'ini'
+ },
+ PostsViewJumpTypes: {
+ BOTTOM: 1,
+ POST: 2,
+ SIDEBAR_OPEN: 3
}
};
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index c7c8549b9..296307bc6 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -884,6 +884,23 @@ export function getDisplayName(user) {
return user.username;
}
+export function displayUsername(userId) {
+ const user = UserStore.getProfile(userId);
+ const nameFormat = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'false'}).value;
+
+ let username = '';
+ if (nameFormat === 'nickname_full_name') {
+ username = user.nickname || getFullName(user);
+ } else if (nameFormat === 'full_name') {
+ username = getFullName(user);
+ }
+ if (!username.trim().length) {
+ username = user.username;
+ }
+
+ return username;
+}
+
//IE10 does not set window.location.origin automatically so this must be called instead when using it
export function getWindowLocationOrigin() {
var windowLocationOrigin = window.location.origin;
@@ -976,7 +993,7 @@ export function isBrowserIE() {
}
export function isBrowserEdge() {
- return window.naviagtor && navigator.userAgent && navigator.userAgent.toLowerCase().indexOf('edge') > -1;
+ return window.navigator && navigator.userAgent && navigator.userAgent.toLowerCase().indexOf('edge') > -1;
}
export function getDirectChannelName(id, otherId) {
diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss
index 635928fe3..c286927a2 100644
--- a/web/sass-files/sass/partials/_base.scss
+++ b/web/sass-files/sass/partials/_base.scss
@@ -9,29 +9,37 @@ body {
position: relative;
height: 100%;
&.white {
- background: #fff;
- > .container-fluid {
- overflow: auto;
- }
- .inner__wrap {
- > .row.content {
- min-height: 100%;
- margin-bottom: -89px;
+ background: #fff;
+ > .container-fluid {
+ overflow: auto;
+ }
+ .inner__wrap {
+ > .row.content {
+ min-height: 100%;
+ margin-bottom: -89px;
+ }
}
- }
}
- .inner__wrap {
+}
+
+.inner__wrap {
height: 100%;
> .row.main {
- height: 100%;
- position: relative;
+ height: 100%;
+ position: relative;
}
- }
- > .container-fluid {
+}
+
+.container-fluid {
+ @include clearfix;
+ height: 100%;
+ position: relative;
+}
+
+.channel-view {
@include clearfix;
height: 100%;
position: relative;
- }
}
img {
diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss
index 7e776bf76..feb392234 100644
--- a/web/sass-files/sass/partials/_headers.scss
+++ b/web/sass-files/sass/partials/_headers.scss
@@ -1,3 +1,6 @@
+#channel-header {
+ padding: 3px 0;
+}
.row {
&.header {
position: relative;
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index 7709e17f3..e11f9b640 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -441,7 +441,10 @@ body.ios {
&.post-profile-img__container {
float: left;
.post-profile-img {
+ width: 36px;
+ height: 36px;
margin-right: 10px;
+ vertical-align: inherit;
@include border-radius(50px);
}
}
diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss
index b85fa638a..8f353c401 100644
--- a/web/sass-files/sass/partials/_responsive.scss
+++ b/web/sass-files/sass/partials/_responsive.scss
@@ -763,7 +763,7 @@
.channel__wrap & {
padding-top: 45px;
}
- .channel-header {
+ #channel-header {
display: none;
}
}
diff --git a/web/sass-files/sass/partials/_search.scss b/web/sass-files/sass/partials/_search.scss
index e50dc398a..73b69c866 100644
--- a/web/sass-files/sass/partials/_search.scss
+++ b/web/sass-files/sass/partials/_search.scss
@@ -1,5 +1,8 @@
+#channel-header .search-bar__container {
+ padding: 8px 8px 8px 0;
+}
.search-bar__container {
- padding: 8px 8px 8px 0;
+ padding: 12px 8px 12px 0;
}
.search__clear {
display: none;
diff --git a/web/static/help/about.html b/web/static/help/about.html
index 4659aa9cc..6bdbee27e 100644
--- a/web/static/help/about.html
+++ b/web/static/help/about.html
@@ -5,7 +5,7 @@
</p>
<p>We built Mattermost to help teams focus on what matters most to them. It works for us, we hope it works for you too.
-Learn more, or download the source code from <a href=http://mattermost.com>http://mattermost.com</a>.</p>
+Learn more, or download the source code from <a href=http://mattermost.org>http://mattermost.org</a>.</p>
<h1>Join the community</h1>
<p>To take part in the community building Mattermost, please consider sharing comments, feature requests, votes, and contributions. If you like the project, please Tweet about us at <a href=https://twitter.com/mattermosthq>@mattermosthq</a>.</p>
diff --git a/web/templates/channel.html b/web/templates/channel.html
index 4b8318d43..63fe38587 100644
--- a/web/templates/channel.html
+++ b/web/templates/channel.html
@@ -5,24 +5,7 @@
{{template "head" . }}
<body>
<div id="error_bar"></div>
- <div class="container-fluid">
- <div class="sidebar--right" id="sidebar-right"></div>
- <div class="sidebar--menu" id="sidebar-menu"></div>
- <div class="sidebar--left" id="sidebar-left"></div>
- <div class="inner__wrap channel__wrap">
- <div class="row header">
- <div id="navbar"></div>
- </div>
- <div class="row main">
- <div id="file_upload_overlay"></div>
- <div id="app-content" class="app__content">
- <div id="channel-header"></div>
- <div id="post-list"></div>
- <div class="post-create__container" id="post-create"></div>
- </div>
- </div>
- </div>
- </div>
+ <div id="channel_view" class="channel-view"></div>
<div id="channel_loader"></div>
<div id="post_mention_tab"></div>
<div id="reply_mention_tab"></div>
diff --git a/web/web.go b/web/web.go
index 424adea93..34f4df08f 100644
--- a/web/web.go
+++ b/web/web.go
@@ -132,7 +132,7 @@ func watchAndParseTemplates() {
}
}
-var browsersNotSupported string = "MSIE/8;MSIE/9;Internet Explorer/8;Internet Explorer/9"
+var browsersNotSupported string = "MSIE/8;MSIE/9;MSIE/10;Internet Explorer/8;Internet Explorer/9;Internet Explorer/10;Safari/7"
func CheckBrowserCompatability(c *api.Context, r *http.Request) bool {
ua := user_agent.New(r.UserAgent())
@@ -143,7 +143,7 @@ func CheckBrowserCompatability(c *api.Context, r *http.Request) bool {
version := strings.Split(browser, "/")
if strings.HasPrefix(bname, version[0]) && strings.HasPrefix(bversion, version[1]) {
- c.Err = model.NewAppError("CheckBrowserCompatability", "Your current browser is not supported, please upgrade to one of the following browsers: Google Chrome 21 or higher, Internet Explorer 10 or higher, FireFox 14 or higher", "")
+ c.Err = model.NewAppError("CheckBrowserCompatability", "Your current browser is not supported, please upgrade to one of the following browsers: Google Chrome 21 or higher, Internet Explorer 11 or higher, FireFox 14 or higher, Safari 8 or higher", "")
return false
}
}