summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml2
-rw-r--r--CHANGELOG.md5
-rw-r--r--api/post.go63
-rw-r--r--api/post_test.go48
-rw-r--r--api/preference.go2
-rw-r--r--api/preference_test.go2
-rw-r--r--api/templates/post_body.html2
-rw-r--r--api/web_team_hub.go4
-rw-r--r--doc/help/Search.md3
-rw-r--r--doc/install/Configuration-Settings.md4
-rw-r--r--doc/integrations/webhooks/Incoming-Webhooks.md4
-rw-r--r--doc/process/release-process.md141
-rw-r--r--docker/1.1/Dockerrun.aws.zipbin710 -> 0 bytes
-rw-r--r--docker/1.3/Dockerfile (renamed from docker/1.1/Dockerfile)2
-rw-r--r--docker/1.3/Dockerrun.aws.zipbin0 -> 867 bytes
-rw-r--r--docker/1.3/Dockerrun.aws/.ebextensions/01_files.config (renamed from docker/1.1/Dockerrun.aws/.ebextensions/01_files.config)0
-rwxr-xr-xdocker/1.3/Dockerrun.aws/Dockerrun.aws.json (renamed from docker/1.1/Dockerrun.aws/Dockerrun.aws.json)2
-rw-r--r--docker/1.3/README.md23
-rw-r--r--docker/1.3/config_docker.json (renamed from docker/1.1/config_docker.json)12
-rwxr-xr-xdocker/1.3/docker-entry.sh (renamed from docker/1.1/docker-entry.sh)0
-rw-r--r--model/config.go17
-rw-r--r--model/message.go17
-rw-r--r--model/outgoing_webhook.go2
-rw-r--r--model/outgoing_webhook_test.go2
-rw-r--r--model/preference.go2
-rw-r--r--model/preference_test.go2
-rw-r--r--model/preferences.go2
-rw-r--r--model/utils.go12
-rw-r--r--model/utils_test.go9
-rw-r--r--model/version.go1
-rw-r--r--store/sql_channel_store.go20
-rw-r--r--store/sql_post_store.go2
-rw-r--r--store/sql_preference_store.go2
-rw-r--r--store/sql_preference_store_test.go2
-rw-r--r--store/store.go1
-rw-r--r--utils/config.go1
-rw-r--r--web/react/components/edit_channel_header_modal.jsx52
-rw-r--r--web/react/components/invite_member_modal.jsx19
-rw-r--r--web/react/components/post_attachment_oembed.jsx9
-rw-r--r--web/react/components/post_body.jsx6
-rw-r--r--web/react/components/providers.json4
-rw-r--r--web/react/components/sidebar.jsx50
-rw-r--r--web/react/components/team_general_tab.jsx133
-rw-r--r--web/react/components/user_settings/manage_outgoing_hooks.jsx2
-rw-r--r--web/react/dispatcher/event_helpers.jsx7
-rw-r--r--web/react/stores/channel_store.jsx4
-rw-r--r--web/react/stores/preference_store.jsx14
-rw-r--r--web/react/stores/socket_store.jsx9
-rw-r--r--web/react/utils/client.jsx7
-rw-r--r--web/react/utils/constants.jsx4
-rw-r--r--web/react/utils/utils.jsx9
-rw-r--r--web/sass-files/sass/partials/_modal.scss1
-rw-r--r--web/sass-files/sass/partials/_settings.scss4
53 files changed, 529 insertions, 218 deletions
diff --git a/.travis.yml b/.travis.yml
index 7e54d3335..c88267206 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -17,6 +17,8 @@ addons:
- 127.0.0.1 dockerhost
before_deploy:
- sudo rm -rf dist/mattermost
+ - sudo chown `whoami` dist/mattermost.tar.gz
+ - sudo chmod 777 dist/mattermost.tar.gz
- rvm 1.9.3 do gem install mime-types -v 2.6.2
deploy:
# Github releases, builds only on tags
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 25d188397..a9dad1798 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -70,8 +70,8 @@ Multiple settings were added to [`config.json`](./config/config.json). These opt
- Removed: `"ApplePushServer": ""` which is replaced with `SendPushNotifications` and `PushNotificationServer`
- Removed: `"ApplePushCertPublic": ""` which is replaced with `SendPushNotifications` and `PushNotificationServer`
- Removed: `"ApplePushCertPrivate": ""` which is replaced with `SendPushNotifications` and `PushNotificationServer`
- - Added: `"SendPushNotifications": true` to control whether mobile push notifications are sent to the server specified in `PushNotificationServer`
- - Added: `"PushNotificationServer": "https://push.mattermost.com"` to specify the address of the proxy server that re-sends push notifications to their respective services like APNS (Apple Push Notification Services)
+ - Added: `"SendPushNotifications": false` to control whether mobile push notifications are sent to the server specified in `PushNotificationServer`
+ - Added: `"PushNotificationServer": ""` to specify the address of the proxy server that re-sends push notifications to their respective services like APNS (Apple Push Notification Services)
#### Known Issues
@@ -81,6 +81,7 @@ Multiple settings were added to [`config.json`](./config/config.json). These opt
- No scroll bar in center channel
- Pasting images into text box fails to upload on Firefox, Safari, and IE11
- Slack import @mentions break
+- Usernames containing a "." do not get mention notifications
#### Contributors
diff --git a/api/post.go b/api/post.go
index 6736d75e2..97211b391 100644
--- a/api/post.go
+++ b/api/post.go
@@ -236,9 +236,68 @@ func handlePostEventsAndForget(c *Context, post *model.Post, triggerWebhooks boo
if triggerWebhooks {
handleWebhookEventsAndForget(c, post, team, channel, user)
}
+
+ if channel.Type == model.CHANNEL_DIRECT {
+ go makeDirectChannelVisible(c.Session.TeamId, post.ChannelId)
+ }
}()
}
+func makeDirectChannelVisible(teamId string, channelId string) {
+ var members []model.ChannelMember
+ if result := <-Srv.Store.Channel().GetMembers(channelId); result.Err != nil {
+ l4g.Error("Failed to get channel members channel_id=%v err=%v", channelId, result.Err.Message)
+ return
+ } else {
+ members = result.Data.([]model.ChannelMember)
+ }
+
+ if len(members) != 2 {
+ l4g.Error("Failed to get 2 members for a direct channel channel_id=%v", channelId)
+ return
+ }
+
+ // make sure the channel is visible to both members
+ for i, member := range members {
+ otherUserId := members[1-i].UserId
+
+ if result := <-Srv.Store.Preference().Get(member.UserId, model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, otherUserId); result.Err != nil {
+ // create a new preference since one doesn't exist yet
+ preference := &model.Preference{
+ UserId: member.UserId,
+ Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
+ Name: otherUserId,
+ Value: "true",
+ }
+
+ if saveResult := <-Srv.Store.Preference().Save(&model.Preferences{*preference}); saveResult.Err != nil {
+ l4g.Error("Failed to save direct channel preference user_id=%v other_user_id=%v err=%v", member.UserId, otherUserId, saveResult.Err.Message)
+ } else {
+ message := model.NewMessage(teamId, channelId, member.UserId, model.ACTION_PREFERENCE_CHANGED)
+ message.Add("preference", preference.ToJson())
+
+ PublishAndForget(message)
+ }
+ } else {
+ preference := result.Data.(model.Preference)
+
+ if preference.Value != "true" {
+ // update the existing preference to make the channel visible
+ preference.Value = "true"
+
+ if updateResult := <-Srv.Store.Preference().Save(&model.Preferences{preference}); updateResult.Err != nil {
+ l4g.Error("Failed to update direct channel preference user_id=%v other_user_id=%v err=%v", member.UserId, otherUserId, updateResult.Err.Message)
+ } else {
+ message := model.NewMessage(teamId, channelId, member.UserId, model.ACTION_PREFERENCE_CHANGED)
+ message.Add("preference", preference.ToJson())
+
+ PublishAndForget(message)
+ }
+ }
+ }
+ }
+}
+
func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team, channel *model.Channel, user *model.User) {
go func() {
if !utils.Cfg.ServiceSettings.EnableOutgoingWebhooks {
@@ -477,8 +536,7 @@ func sendNotificationsAndForget(c *Context, post *model.Post, team *model.Team,
teamURL := c.GetSiteURL() + "/" + team.Name
// Build and send the emails
- location, _ := time.LoadLocation("UTC")
- tm := time.Unix(post.CreateAt/1000, 0).In(location)
+ tm := time.Unix(post.CreateAt/1000, 0)
subjectPage := NewServerTemplatePage("post_subject")
subjectPage.Props["SiteURL"] = c.GetSiteURL()
@@ -510,6 +568,7 @@ func sendNotificationsAndForget(c *Context, post *model.Post, team *model.Team,
bodyPage.Props["Minute"] = fmt.Sprintf("%02d", tm.Minute())
bodyPage.Props["Month"] = tm.Month().String()[:3]
bodyPage.Props["Day"] = fmt.Sprintf("%d", tm.Day())
+ bodyPage.Props["TimeZone"], _ = tm.Zone()
bodyPage.Props["PostMessage"] = model.ClearMentionTags(post.Message)
bodyPage.Props["TeamLink"] = teamURL + "/channels/" + channel.Name
diff --git a/api/post_test.go b/api/post_test.go
index 0cb437e88..8e09ca76d 100644
--- a/api/post_test.go
+++ b/api/post_test.go
@@ -805,3 +805,51 @@ func TestFuzzyPosts(t *testing.T) {
}
}
}
+
+func TestMakeDirectChannelVisible(t *testing.T) {
+ Setup()
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user1.Id))
+
+ user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user2.Id))
+
+ // user2 will be created with prefs created to show user1 in the sidebar so set that to false to get rid of it
+ Client.LoginByEmail(team.Name, user2.Email, "pwd")
+
+ preferences := &model.Preferences{
+ {
+ UserId: user2.Id,
+ Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
+ Name: user1.Id,
+ Value: "false",
+ },
+ }
+ Client.Must(Client.SetPreferences(preferences))
+
+ Client.LoginByEmail(team.Name, user1.Email, "pwd")
+
+ channel := Client.Must(Client.CreateDirectChannel(map[string]string{"user_id": user2.Id})).Data.(*model.Channel)
+
+ makeDirectChannelVisible(team.Id, channel.Id)
+
+ if result, err := Client.GetPreference(model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, user2.Id); err != nil {
+ t.Fatal("Errored trying to set direct channel to be visible for user1")
+ } else if pref := result.Data.(*model.Preference); pref.Value != "true" {
+ t.Fatal("Failed to set direct channel to be visible for user1")
+ }
+
+ Client.LoginByEmail(team.Name, user2.Email, "pwd")
+
+ if result, err := Client.GetPreference(model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, user1.Id); err != nil {
+ t.Fatal("Errored trying to set direct channel to be visible for user2")
+ } else if pref := result.Data.(*model.Preference); pref.Value != "true" {
+ t.Fatal("Failed to set direct channel to be visible for user2")
+ }
+}
diff --git a/api/preference.go b/api/preference.go
index 6d6ac1a7f..e9c74aafe 100644
--- a/api/preference.go
+++ b/api/preference.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api
diff --git a/api/preference_test.go b/api/preference_test.go
index 2f6204246..310b4dcdb 100644
--- a/api/preference_test.go
+++ b/api/preference_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api
diff --git a/api/templates/post_body.html b/api/templates/post_body.html
index 182134b1a..00d4a563d 100644
--- a/api/templates/post_body.html
+++ b/api/templates/post_body.html
@@ -18,7 +18,7 @@
<tr>
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
<h2 style="font-weight: normal; margin-top: 10px;">You were mentioned</h2>
- <p>CHANNEL: {{.Props.ChannelName}}<br>{{.Props.SenderName}} - {{.Props.Hour}}:{{.Props.Minute}} GMT, {{.Props.Month}} {{.Props.Day}}<br><pre style="text-align:left;font-family: 'Lato', sans-serif; white-space: pre-wrap; white-space: -moz-pre-wrap; white-space: -pre-wrap; white-space: -o-pre-wrap; word-wrap: break-word;">{{.Props.PostMessage}}</pre></p>
+ <p>CHANNEL: {{.Props.ChannelName}}<br>{{.Props.SenderName}} - {{.Props.Hour}}:{{.Props.Minute}} {{.Props.TimeZone}}, {{.Props.Month}} {{.Props.Day}}<br><pre style="text-align:left;font-family: 'Lato', sans-serif; white-space: pre-wrap; white-space: -moz-pre-wrap; white-space: -pre-wrap; white-space: -o-pre-wrap; word-wrap: break-word;">{{.Props.PostMessage}}</pre></p>
<p style="margin: 20px 0 15px">
<a href="{{.Props.TeamLink}}" style="background: #2389D7; display: inline-block; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 170px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Go To Channel</a>
</p>
diff --git a/api/web_team_hub.go b/api/web_team_hub.go
index 6a25b7d3d..2c2386317 100644
--- a/api/web_team_hub.go
+++ b/api/web_team_hub.go
@@ -95,9 +95,11 @@ func ShouldSendEvent(webCon *WebConn, msg *model.Message) bool {
return false
}
} else {
- // Don't share a user's view events with other users
+ // Don't share a user's view or preference events with other users
if msg.Action == model.ACTION_CHANNEL_VIEWED {
return false
+ } else if msg.Action == model.ACTION_PREFERENCE_CHANGED {
+ return false
}
// Only report events to a user who is the subject of the event, or is in the channel of the event
diff --git a/doc/help/Search.md b/doc/help/Search.md
index 51095aac6..354e29a43 100644
--- a/doc/help/Search.md
+++ b/doc/help/Search.md
@@ -7,7 +7,8 @@ The search box in Mattermost brings back results from any channel of which youâ€
- Multiple search terms are connected with “OR” by default. Typing in `Mattermost website` returns results containing “Mattermost” or “website”
- Use `from:` to find posts from specific users and `in:` to find posts in specific channels. For example: Searching `Mattermost in:town-square` only returns messages in Town Square that contain `Mattermost`
- Use quotes to return search results for exact terms. For example: Searching `"Mattermost website"` returns messages containing the entire phrase `"Mattermost website"` and not messages containing only `Mattermost` or `website`
-- Use the `*` character for wildcard searches that match within words. For example: Searching for `rea*` brings back messages containing `reach`, `reason` and other words starting with `rea`.
+- Use the `*` character for wildcard searches that match within words. For example: Searching for `rea*` brings back messages containing `reach`, `reason` and other words starting with `rea`
+- Click on the **Jump** link on the right of a search result to view the post archive in the center channel
#### Limitations:
diff --git a/doc/install/Configuration-Settings.md b/doc/install/Configuration-Settings.md
index 46a6defdf..5f9b3b62d 100644
--- a/doc/install/Configuration-Settings.md
+++ b/doc/install/Configuration-Settings.md
@@ -146,10 +146,10 @@ Port of SMTP email server.
#### Push Notification Settings
-```"SendPushNotifications": true```
+```"SendPushNotifications": false```
"true": Your mattermsot server sends mobile push notifications to the server specified in **PushNotificationServer**; "false": Mobile push notifications are disabled.
-```"PushNotificationServer": "https://push.mattermost.com"```
+```"PushNotificationServer": ""```
Address of the proxy server that re-sends push notifications to their respective services like APNS (Apple Push Notification Services).
diff --git a/doc/integrations/webhooks/Incoming-Webhooks.md b/doc/integrations/webhooks/Incoming-Webhooks.md
index 5e93c4ac7..7373892ad 100644
--- a/doc/integrations/webhooks/Incoming-Webhooks.md
+++ b/doc/integrations/webhooks/Incoming-Webhooks.md
@@ -1,6 +1,6 @@
# Incoming Webhooks
-Incoming webhooks allow external applications, written in the programming language of your choice--to post messages into Mattermost channels and private groups by sending a specifically formatted JSON payload via HTTP POST request to a secret Mattermost URL generated specifically for each application.
+Incoming webhooks allow external applications, written in the programming language of your choice--to post messages into Mattermost channels, private groups and direct messages by sending a specifically formatted JSON payload via HTTP POST request to a secret Mattermost URL generated specifically for each application.
A couple key points:
@@ -69,7 +69,7 @@ Additional Notes:
1. For the HTTP request body, if `Content-Type` is specified as `application/json` in the headers of the HTTP request then the body of the request can be direct JSON. For example, ```{"text": "Hello, this is some text."}```
-2. You can override the channel specified in the webhook definition by specifying a `channel` parameter in your payload. For example, you might have a single webhook created for _Town Square_, but you can use ```payload={"channel": "off-topic", "text": "Hello, this is some text."}``` to send a message to the _Off-Topic_ channel using the same webhook URL
+2. You can override the channel specified in the webhook definition by specifying a `channel` parameter in your payload. For example, you might have a single webhook created for _Town Square_, but you can use ```payload={"channel": "off-topic", "text": "Hello, this is some text."}``` to send a message to the _Off-Topic_ channel using the same webhook URL. If an `@` symbol followed by a username is specified, then the message will be sent to that user's direct message channel
1. In addition, with **Enable Overriding of Usernames from Webhooks** turned on, you can also override the username the message posts as by providing a `username` parameter in your JSON payload. For example, you might want your message looking like it came from a robot so you can use ```payload={"username": "robot", "text": "Hello, this is some text."}``` to change the username of the post to robot. Note, to combat any malicious users from trying to use this to perform [phishing attacks](https://en.wikipedia.org/wiki/Phishing) a `BOT` indicator appears next to posts coming from webhooks
diff --git a/doc/process/release-process.md b/doc/process/release-process.md
index 96bd2b050..294f5aa79 100644
--- a/doc/process/release-process.md
+++ b/doc/process/release-process.md
@@ -1,68 +1,81 @@
-We're working on making internal processes in the Mattermost core team more transparent for the community. Below is a working draft of our software development process, which will be updated live as we refine our process.
-
-Questions, feedback, comments always welcome,
-
-----------
+# Mattermost Release Process
Mattermost core team works on a monthly release process, with a new version shipping on the 16th of each month.
This document outlines the development process for the Mattermost core team, which draws from what we find works best for us from Agile, Scrum and Software Development Lifecycle approaches.
-This is a working document that will update as our process evolves.
-
+Notes:
+- All cut-off dates are based on 10am PST (UTC-07/08) on the day stated.
+- T-minus counts are measured in "working days" (weekdays other than major holidays concurrent in US and Canada) prior to release day.
-### - Beginning of release
-- (Ops) Queue an agenda item for first team meeting of the release to review Roadmap
+### (Code complete date of previous release) Beginning of release
+- Pre-work for the current release begins at the code complete date of the previous release. See "Code Complete" section below for details.
-### - (10 weekdays before release date) Cut-off for major features
-- No major features can be committed to the current release after this date
-- (Dev) Prioritize reviewing, updating, and merging of all pull requests that are going to make it into the release
- - There should be no more tickets in the [pull request queue](https://github.com/mattermost/platform/pulls) marked for the current release
+### (T-minus 10 working days) Cut-off for major features
+- No pull requests for major features should be submitted to the current release after this date (except if release manager decides to add "release-exception" label)
+- (Ops) Post this checklist in Release channel
+- (PM) Write compatibility updates for config.json and database changes [See example](https://github.com/mattermost/platform/blob/master/CHANGELOG.md#compatibility)
+- (PM) Confirm changes to config.json in compatibility section of Changelog are written back to [settings documentation](https://github.com/mattermost/platform/blob/master/doc/install/Configuration-Settings.md)
+- (Dev) Prioritize reviewing, updating, and merging of pull requests for current release until there are no more tickets in the [pull request queue](https://github.com/mattermost/platform/pulls) marked for the current release
- (Leads) Meets to prioritize the final tickets of the release
- Backlog is reviewed and major features that won’t make it are moved to next release
- Triage tickets
- - Review roadmap for next release
-- (Marketing) Writes the "Highlights" section of the Changelog
-- (PM) Write compatibility updates for config.json and database changes [See example](https://github.com/mattermost/platform/blob/master/CHANGELOG.md#compatibility)
+ - Finalize roadmap for next release
+ - Draft roadmap for release after next (used to prioritize design tasks)
+- (Marketing) Drafts marketing bullet points for next release based off of roadmap
+- (Marketing) Submits pull request for "Highlights" section of the Changelog
+- (Marketing) Notes date of announcements in release channel
- (PM) Update [Upgrade Guide](https://github.com/mattermost/platform/blob/master/doc/install/Upgrade-Guide.md) for any steps needed to upgrade to new version
-- (PM) Prepare tickets for cutting RCs builds, filing issue in GitLab omnibus to take RC candidate, testing GitLab RC with Mattermost
-- (Stand-up) Each team member discusses worst bug
-
-### - (8 weekdays before release date) Feature Complete and Stabilization
-- After the cut-off time for Feature Complete, Dev prioritizes reviewing PRs and committing to master so Stabilization period can begin, with testing and high priority bug fixes
-- During Stabilization period only BUGS can be committed to master, non-bug tickets are tagged for next version and wait until after a release candidate is cut to be added to master
- - (PM) Review all [S1 bugs](https://mattermost.atlassian.net/secure/IssueNavigator.jspa?mode=hide&requestId=10600) and mark important ones as high priority
- - (Dev + PM) Exceptions can be made by triage team consensus across PM and Dev. List of approved changes for release candidate 1 here: https://mattermost.atlassian.net/issues/?filter=10204
-- (PM) Documentation
+- (PM) Prepare tickets for [cutting RC builds](https://mattermost.atlassian.net/browse/PLT-985), [creating the final release candidate](https://mattermost.atlassian.net/browse/PLT-986), [creating AMIs](https://mattermost.atlassian.net/browse/PLT-1213), and [testing GitLab RC with Mattermost](https://mattermost.atlassian.net/browse/PLT-1013)
+- (Stand-up) Each team member discusses worst bug (10-15s)
+
+### (T-minus 8 working days) Feature Complete and Stabilization
+- No pull requests for features can be submitted to the current release after this date (except if release manager add "release-exception" label to Jira ticket)
+- (Ops) Post this checklist in Release channel
+- (Dev) Prioritize reviewing, updating, and merging of pull requests for current release until there are no more tickets in the [pull request queue](https://github.com/mattermost/platform/pulls) marked for the current release
+- **Stablization** period begins when all features for release have been committed.
+ - During this period, only **bugs** can be committed to master. Non-bug pull requests are tagged for next version and wait until after a release candidate is cut to be committed to master
+ - (RM) Exceptions can be made by release manager by setting priority to "Highest" and labelling "release-exception", which will add ticket to [Hotfix list for release candidate](https://mattermost.atlassian.net/issues/?filter=10204).
+ - (PM) Review all [Severity 1 bugs (data loss or security)](https://mattermost.atlassian.net/secure/IssueNavigator.jspa?mode=hide&requestId=10600) to consider for adding to Hotfix list.
+- (PM) Complete documentation
- (PM) Make Changelog PR with updates for latest feature additions and changes
- (PM) Make Changelog PR with updates to contributors
- (PM) Make NOTICE.txt PR for any new libraries added from dev, if not added already
- (PM) Prioritize any developer documentation tickets
-- (PM and devs) Sign-off testing of their feature areas (i.e. PM/dev either signs-off that their area is well tested, or they flag that potential quality issues may exist)
-- (Ops) Mail out mugs to any new contributors
-- (Team) Select "Top Contributor" for the release from external contributions to be mentioned in release announcement
-- (Marketing) Decides announce date (discuss in meeting)
+ - (PM) Draft [GitLab ticket](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/942) to take next Mattermost version in the Omnibus, but do not post until RC1 is cut
+- (PM) Check testing is complete
+ - (PM) Works with Ops to check the [Quality Gate](https://github.com/mattermost/process/blob/master/release/quality-gates.md) for feature complete
+ - (PM + Dev) Sign-off testing of their feature areas (i.e. PM/dev either signs-off that their area is well tested, or they flag that potential quality issues may exist)
+- **(Team) Feature Complete Meeting (10:15am PST)**
+ - (PM) Leads review of Changelog
+ - (Team) Each team member discusses worst bug (10-15s)
+ - (PM) Review feature list for next release
+ - (Marketing) Share draft of marketing announce for next release
+- (Marketing) Communicates checklist of items needed by specific dates to write the blog post announce (e.g. screenshots, GIFs,
- (Ops) Post Announce Date in Release channel + update the channel header to reflect date
-- (Marketing) Communicates checklist of items needed by specific dates to write the blog post announce (e.g. screenshots, GIFs, documentation) and begins to write the blog post, tweet, and email for the release announcement
-- (PM) Works with Ops to check the Quality Gate for feature complete
-- (PM) Communicate to team the plan for next release
-- (Stand-up) Each team member discusses worst bug
+- (Ops) Mail out mugs to any new contributors
-### - (5 weekdays before release date) Code Complete and Release Candidate Cut
-- (Team) Meets to discuss release at 10am PST
- - (PM) Each area changed in latest release is assigned a PM owner to lead testing
- - (Ops) Walks through each item of the **Code Complete and Release Candidate Cut** checklist
+### (T-minus 5 working days) Code Complete and Release Candidate Cut
+- (Ops) Post this checklist in Release channel
+- (Ops) For the next release, create team meetings on Feature Complete and Code Complete dates
+- (PM) Remove "Under Development" notice for current release from Changelog on master
+- **(Team) Code Complete Meeting (10:15am PST meeting)**
+ - (Ops) Walks through each item of this checklist
+ - (PM) Assigns each area of the release testing spreadsheet to a team member
- (Dev) Last check of tickets that need to be merged before RC1
- - (Team) Each team member discusses worst bug
-- After 10am PST meeting the release is considered “Code Complete”.
- - (Dev) Completes final reviews and updates of PRs marked for the release version
- - There should be no more tickets in the [pull request queue](https://github.com/mattermost/platform/pulls) marked for the current release
- - Master is tagged and branched and “Release Candidate 1″ is cut (e.g. 1.1.0-RC1) according to the Release Candidate Checklist
+ - (Team) Each team member discusses worst bug (10-15s)
+- **Code Complete** is declared after meeting
+ - (Dev) Prioritize reviewing, updating, and merging of pull requests for current release until there are no more tickets in the [pull request queue](https://github.com/mattermost/platform/pulls) marked for the current release
+ - (Build) Master is tagged and branched and “Release Candidate 1″ is cut (e.g. 1.1.0-RC1) according to the [Release Candidate Checklist](https://github.com/mattermost/process/blob/master/release/create-release-candidate.md)
- (PM) Create meta issue for regressions in GitHub (see [example](https://github.com/mattermost/platform/issues/574))
-
-### - (4 weekdays before release date) Release Candidate Testing
-- Final testing is conducted by the team on the acceptance server and any issues found are filed
- - (Dev) Tests upgrade from previous version to current version, following the [Upgrade Guide](https://github.com/mattermost/platform/blob/master/doc/install/Upgrade-Guide.md)
+ - (PM) Include link to meta-issue in release notes of RC1
+ - (PM) Tweet announcement that RC1 is ready (see [example](https://twitter.com/mattermosthq/status/664172166368264192))
+ - (PM) Submit GitLab ticket to take next Mattermost version in the Omnibus
+
+### (T-minus 4 working days) Release Candidate Testing
+- (Team) Final testing is conducted by the team on the acceptance server and any issues found are filed
+- (Build) Tests upgrade from previous version to current version, following the [Upgrade Guide](https://github.com/mattermost/platform/blob/master/doc/install/Upgrade-Guide.md)
+ - Database upgrade should be tested on both MySQL and Postgres
- (Ops) Posts copy of the **Release Candidate Testing** checklist into Town Square in PRODUCTION
- (Ops) Moves meeting, test and community channels over to the production version of RC, and posts in Town Square asking everyone to move communication over to the new team for testing purposes
- (PM) Test feature areas and post bugs to Bugs/Issues in PRODUCTION
@@ -72,6 +85,7 @@ This is a working document that will update as our process evolves.
- (PM) Posts links to all issues found in RC as comments on the meta issue
- (PM) Updates description to include approved fixes
- (PM) Posts screenshot and link to final tickets for next RC to the Release room
+ - (PM) Updates Release Notes with any new issues that will not be fixed for the current version
- (PM & DEV leads) Triage hotfix candidates and decide on whether and when to cut next RC or final
- (Dev) PRs for hotfixes made to release branch, and changes from release branch are merged into master
- (Ops) Tests approved fixes on master
@@ -81,19 +95,28 @@ This is a working document that will update as our process evolves.
- (Ops) verifies each of the issues in meta ticket is fixed
- (PM) If no blocking issues are found, PM, Dev and Ops signs off on the release
-### - (2 weekdays before release date) Release
- - (Dev) Tags a new release (e.g. 1.1.0) and runs an official build which should be essentially identical to the last RC
+### (T-minus 2 working days) Release Build Cut
+- (Ops) Post this checklist in Release channel
+- (Build) Tags a new release (e.g. 1.1.0) and runs an official build which should be essentially identical to the last RC
- (PM) Any significant issues that were found and not fixed for the final release are noted in the release notes
- - If an urgent and important issue needs to be addressed between major releases, a hotfix release (e.g. 1.1.1) may be released, however this should be very rare, given a monthly cadence
- - (PM) Copy and paste the Release Notes from the Changelog to the Release Description
- - (PM) Update the mattermost.org/download page
- - (Dev) Delete RCs after final version is shipped
- - (PM) Close final GitHub RC meta ticket
-
-### - (0 weekdays before release date) End of Release
-- (PM) Makes sure marketing has been posted (animated GIFs, screenshots, mail announcement, Tweets, blog posts)
+ - If an urgent and important issue needs to be addressed between major releases, a bug fix release (e.g. 1.1.1) may be created
+- (PM) Copy and paste the Release Notes from the Changelog to the Release Description
+- (PM) Update the mattermost.org/download page
+- (PM) Update the AMI links on mattermost.org/installation
+- (PM) Close final GitHub RC meta ticket
+- (Dev) Delete RCs after final version is shipped
+- (Marketing) Finalize marketing
+ - (Marketing) Finalize mailchimp email blast
+ - (Marketing) Finalize blog post and put on timer for release
+ - (Marketing) Finalize tweet announcement
+ - (Marketing) Finalize announcement on general mailing list
+ - (Marketing) Finalize announcement for gitlab.mattermost.com
+
+### (T-minus 0 working days) Release Day
+- (Ops) Post this checklist in Release channel
+- (PM) Confirm marketing has been posted (animated GIFs, screenshots, mail announcement, Tweets, blog posts)
- (PM) Close the release in Jira
+- (PM) Set header of next release as UNDER DEVELOPMENT in CHANGELOG on master
- (Dev) Check if any libraries need to be updated for the next release, and if so bring up in weekly team meeting
-- (Ops) Post important dates for the next release in the header of the Release channel
-- (Ops) Queue an agenda item for next team meeting for "Stepping Back" Q&A
-- (Ops) Queue an agenda item for next team meeting for Roadmap review
+- (Ops) Post key dates for the next release in the header of the Release channel
+- (Ops) Queue an agenda item for next team meeting for Release Process Kaizen/Q&A
diff --git a/docker/1.1/Dockerrun.aws.zip b/docker/1.1/Dockerrun.aws.zip
deleted file mode 100644
index 945168a71..000000000
--- a/docker/1.1/Dockerrun.aws.zip
+++ /dev/null
Binary files differ
diff --git a/docker/1.1/Dockerfile b/docker/1.3/Dockerfile
index 92bac6a04..55b69673a 100644
--- a/docker/1.1/Dockerfile
+++ b/docker/1.3/Dockerfile
@@ -34,7 +34,7 @@ VOLUME /var/lib/mysql
WORKDIR /mattermost
# Copy over files
-ADD https://github.com/mattermost/platform/releases/download/v1.1.0/mattermost.tar.gz /
+ADD https://github.com/mattermost/platform/releases/download/v1.3.0-rc1/mattermost.tar.gz /
RUN tar -zxvf /mattermost.tar.gz --strip-components=1 && rm /mattermost.tar.gz
ADD config_docker.json /
ADD docker-entry.sh /
diff --git a/docker/1.3/Dockerrun.aws.zip b/docker/1.3/Dockerrun.aws.zip
new file mode 100644
index 000000000..dd201d990
--- /dev/null
+++ b/docker/1.3/Dockerrun.aws.zip
Binary files differ
diff --git a/docker/1.1/Dockerrun.aws/.ebextensions/01_files.config b/docker/1.3/Dockerrun.aws/.ebextensions/01_files.config
index 7f40a8b34..7f40a8b34 100644
--- a/docker/1.1/Dockerrun.aws/.ebextensions/01_files.config
+++ b/docker/1.3/Dockerrun.aws/.ebextensions/01_files.config
diff --git a/docker/1.1/Dockerrun.aws/Dockerrun.aws.json b/docker/1.3/Dockerrun.aws/Dockerrun.aws.json
index 042e79bd3..d4027e67c 100755
--- a/docker/1.1/Dockerrun.aws/Dockerrun.aws.json
+++ b/docker/1.3/Dockerrun.aws/Dockerrun.aws.json
@@ -1,7 +1,7 @@
{
"AWSEBDockerrunVersion": "1",
"Image": {
- "Name": "mattermost/platform:1.1",
+ "Name": "mattermost/platform:1.3",
"Update": "true"
},
"Ports": [
diff --git a/docker/1.3/README.md b/docker/1.3/README.md
new file mode 100644
index 000000000..f737a1554
--- /dev/null
+++ b/docker/1.3/README.md
@@ -0,0 +1,23 @@
+Mattermost
+==========
+
+http:/mattermost.org
+
+Mattermost is an open-source team communication service. It brings team messaging and file sharing into one place, accessible across PCs and phones, with archiving and search.
+
+Installing Mattermost
+=====================
+
+To run an instance of the latest version of mattermost on your local machine you can run:
+
+`docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform`
+
+To update this image to the latest version you can run:
+
+`docker pull mattermost/platform`
+
+To run an instance of the latest code from the master branch on GitHub you can run:
+
+`docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:dev`
+
+Any questions, please visit http://forum.mattermost.org
diff --git a/docker/1.1/config_docker.json b/docker/1.3/config_docker.json
index 653b6ffd7..a35abb9da 100644
--- a/docker/1.1/config_docker.json
+++ b/docker/1.3/config_docker.json
@@ -5,7 +5,8 @@
"SegmentDeveloperKey": "",
"GoogleDeveloperKey": "",
"EnableOAuthServiceProvider": false,
- "EnableIncomingWebhooks": true,
+ "EnableIncomingWebhooks": false,
+ "EnableOutgoingWebhooks": false,
"EnablePostUsernameOverride": false,
"EnablePostIconOverride": false,
"EnableTesting": false,
@@ -16,7 +17,9 @@
"MaxUsersPerTeam": 50,
"EnableTeamCreation": true,
"EnableUserCreation": true,
- "RestrictCreationToDomains": ""
+ "RestrictCreationToDomains": "",
+ "RestrictTeamNames": true,
+ "EnableTeamListing": false
},
"SqlSettings": {
"DriverName": "mysql",
@@ -65,9 +68,8 @@
"ConnectionSecurity": "",
"InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9YoS",
"PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL",
- "ApplePushServer": "",
- "ApplePushCertPublic": "",
- "ApplePushCertPrivate": ""
+ "SendPushNotifications": false,
+ "PushNotificationServer": ""
},
"RateLimitSettings": {
"EnableRateLimiter": true,
diff --git a/docker/1.1/docker-entry.sh b/docker/1.3/docker-entry.sh
index 6bd2a1263..6bd2a1263 100755
--- a/docker/1.1/docker-entry.sh
+++ b/docker/1.3/docker-entry.sh
diff --git a/model/config.go b/model/config.go
index 14ce444c5..a3ba812b0 100644
--- a/model/config.go
+++ b/model/config.go
@@ -164,6 +164,23 @@ func ConfigFromJson(data io.Reader) *Config {
}
func (o *Config) SetDefaults() {
+
+ if len(o.SqlSettings.AtRestEncryptKey) == 0 {
+ o.SqlSettings.AtRestEncryptKey = NewRandomString(32)
+ }
+
+ if len(o.FileSettings.PublicLinkSalt) == 0 {
+ o.FileSettings.PublicLinkSalt = NewRandomString(32)
+ }
+
+ if len(o.EmailSettings.InviteSalt) == 0 {
+ o.EmailSettings.InviteSalt = NewRandomString(32)
+ }
+
+ if len(o.EmailSettings.PasswordResetSalt) == 0 {
+ o.EmailSettings.PasswordResetSalt = NewRandomString(32)
+ }
+
if o.ServiceSettings.EnableSecurityFixAlert == nil {
o.ServiceSettings.EnableSecurityFixAlert = new(bool)
*o.ServiceSettings.EnableSecurityFixAlert = true
diff --git a/model/message.go b/model/message.go
index 2725353ac..1cb350bbf 100644
--- a/model/message.go
+++ b/model/message.go
@@ -9,14 +9,15 @@ import (
)
const (
- ACTION_TYPING = "typing"
- ACTION_POSTED = "posted"
- ACTION_POST_EDITED = "post_edited"
- ACTION_POST_DELETED = "post_deleted"
- ACTION_CHANNEL_VIEWED = "channel_viewed"
- ACTION_NEW_USER = "new_user"
- ACTION_USER_ADDED = "user_added"
- ACTION_USER_REMOVED = "user_removed"
+ ACTION_TYPING = "typing"
+ ACTION_POSTED = "posted"
+ ACTION_POST_EDITED = "post_edited"
+ ACTION_POST_DELETED = "post_deleted"
+ ACTION_CHANNEL_VIEWED = "channel_viewed"
+ ACTION_NEW_USER = "new_user"
+ ACTION_USER_ADDED = "user_added"
+ ACTION_USER_REMOVED = "user_removed"
+ ACTION_PREFERENCE_CHANGED = "preference_changed"
)
type Message struct {
diff --git a/model/outgoing_webhook.go b/model/outgoing_webhook.go
index 9a1b89a85..0b4fd6bbe 100644
--- a/model/outgoing_webhook.go
+++ b/model/outgoing_webhook.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
diff --git a/model/outgoing_webhook_test.go b/model/outgoing_webhook_test.go
index 0d1cd773e..665b85b6f 100644
--- a/model/outgoing_webhook_test.go
+++ b/model/outgoing_webhook_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
diff --git a/model/preference.go b/model/preference.go
index 4f2ba0099..a3230959c 100644
--- a/model/preference.go
+++ b/model/preference.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
diff --git a/model/preference_test.go b/model/preference_test.go
index 66b7ac50b..e29250bba 100644
--- a/model/preference_test.go
+++ b/model/preference_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
diff --git a/model/preferences.go b/model/preferences.go
index 1ef16151f..f11b5fd80 100644
--- a/model/preferences.go
+++ b/model/preferences.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
diff --git a/model/utils.go b/model/utils.go
index b49b4bb24..5596b06ff 100644
--- a/model/utils.go
+++ b/model/utils.go
@@ -5,6 +5,7 @@ package model
import (
"bytes"
+ "crypto/rand"
"encoding/base32"
"encoding/json"
"fmt"
@@ -81,6 +82,17 @@ func NewId() string {
return b.String()
}
+func NewRandomString(length int) string {
+ var b bytes.Buffer
+ str := make([]byte, length+8)
+ rand.Read(str)
+ encoder := base32.NewEncoder(encoding, &b)
+ encoder.Write(str)
+ encoder.Close()
+ b.Truncate(length) // removes the '==' padding
+ return b.String()
+}
+
// GetMillis is a convience method to get milliseconds since epoch.
func GetMillis() int64 {
return time.Now().UnixNano() / int64(time.Millisecond)
diff --git a/model/utils_test.go b/model/utils_test.go
index 7f14bcdf0..1f1e5f023 100644
--- a/model/utils_test.go
+++ b/model/utils_test.go
@@ -17,6 +17,15 @@ func TestNewId(t *testing.T) {
}
}
+func TestRandomString(t *testing.T) {
+ for i := 0; i < 1000; i++ {
+ r := NewRandomString(32)
+ if len(r) != 32 {
+ t.Fatal("should be 32 chars")
+ }
+ }
+}
+
func TestAppError(t *testing.T) {
err := NewAppError("TestAppError", "message", "")
json := err.ToJson()
diff --git a/model/version.go b/model/version.go
index af99717cd..5e41a28d1 100644
--- a/model/version.go
+++ b/model/version.go
@@ -12,6 +12,7 @@ import (
// It should be maitained in chronological order with most current
// release at the front of the list.
var versions = []string{
+ "1.3.0",
"1.2.1",
"1.2.0",
"1.1.0",
diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go
index 728f44bc9..b68774189 100644
--- a/store/sql_channel_store.go
+++ b/store/sql_channel_store.go
@@ -241,13 +241,27 @@ func (s SqlChannelStore) extraUpdated(channel *model.Channel) StoreChannel {
}
func (s SqlChannelStore) Get(id string) StoreChannel {
+ return s.get(id, false)
+}
+
+func (s SqlChannelStore) GetFromMaster(id string) StoreChannel {
+ return s.get(id, true)
+}
+
+func (s SqlChannelStore) get(id string, master bool) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
- // reading from master due to expected race condition when creating channels
- if obj, err := s.GetMaster().Get(model.Channel{}, id); err != nil {
+ var db *gorp.DbMap
+ if master {
+ db = s.GetMaster()
+ } else {
+ db = s.GetReplica()
+ }
+
+ if obj, err := db.Get(model.Channel{}, id); err != nil {
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)
@@ -439,7 +453,7 @@ func (s SqlChannelStore) SaveMember(member *model.ChannelMember) StoreChannel {
go func() {
var result StoreResult
// Grab the channel we are saving this member to
- if cr := <-s.Get(member.ChannelId); cr.Err != nil {
+ if cr := <-s.GetFromMaster(member.ChannelId); cr.Err != nil {
result.Err = cr.Err
} else {
channel := cr.Data.(*model.Channel)
diff --git a/store/sql_post_store.go b/store/sql_post_store.go
index be770c09e..40dca9930 100644
--- a/store/sql_post_store.go
+++ b/store/sql_post_store.go
@@ -38,8 +38,6 @@ func NewSqlPostStore(sqlStore *SqlStore) PostStore {
}
func (s SqlPostStore) UpgradeSchemaIfNeeded() {
- s.RemoveColumnIfExists("Posts", "ImgCount") // remove after 1.3 release
- s.GetMaster().Exec(`UPDATE Preferences SET Type = :NewType WHERE Type = :CurrentType`, map[string]string{"NewType": model.POST_JOIN_LEAVE, "CurrentType": "join_leave"}) // remove after 1.3 release
}
func (s SqlPostStore) CreateIndexesIfNotExists() {
diff --git a/store/sql_preference_store.go b/store/sql_preference_store.go
index f73dad3ac..307761150 100644
--- a/store/sql_preference_store.go
+++ b/store/sql_preference_store.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package store
diff --git a/store/sql_preference_store_test.go b/store/sql_preference_store_test.go
index 6f8f44f47..ec9d1df6c 100644
--- a/store/sql_preference_store_test.go
+++ b/store/sql_preference_store_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package store
diff --git a/store/store.go b/store/store.go
index 0695ea27f..682195148 100644
--- a/store/store.go
+++ b/store/store.go
@@ -60,6 +60,7 @@ type ChannelStore interface {
SaveDirectChannel(channel *model.Channel, member1 *model.ChannelMember, member2 *model.ChannelMember) StoreChannel
Update(channel *model.Channel) StoreChannel
Get(id string) StoreChannel
+ GetFromMaster(id string) StoreChannel
Delete(channelId string, time int64) StoreChannel
PermanentDeleteByTeam(teamId string) StoreChannel
GetByName(team_id string, domain string) StoreChannel
diff --git a/utils/config.go b/utils/config.go
index 93edc0214..4c8e0b5a1 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -183,6 +183,7 @@ func getClientConfig(c *model.Config) map[string]string {
props["SiteName"] = c.TeamSettings.SiteName
props["EnableTeamCreation"] = strconv.FormatBool(c.TeamSettings.EnableTeamCreation)
+ props["EnableUserCreation"] = strconv.FormatBool(c.TeamSettings.EnableUserCreation)
props["RestrictTeamNames"] = strconv.FormatBool(*c.TeamSettings.RestrictTeamNames)
props["EnableTeamListing"] = strconv.FormatBool(*c.TeamSettings.EnableTeamListing)
diff --git a/web/react/components/edit_channel_header_modal.jsx b/web/react/components/edit_channel_header_modal.jsx
index 209e30fcc..e4817f6e4 100644
--- a/web/react/components/edit_channel_header_modal.jsx
+++ b/web/react/components/edit_channel_header_modal.jsx
@@ -1,8 +1,9 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import * as Client from '../utils/client.jsx';
-import * as AsyncClient from '../utils/async_client.jsx';
+import Constants from '../utils/constants.jsx';
import * as Utils from '../utils/utils.jsx';
const Modal = ReactBootstrap.Modal;
@@ -11,12 +12,14 @@ export default class EditChannelHeaderModal extends React.Component {
constructor(props) {
super(props);
- this.handleEdit = this.handleEdit.bind(this);
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
this.onShow = this.onShow.bind(this);
this.onHide = this.onHide.bind(this);
this.state = {
+ header: props.channel.header,
serverError: ''
};
}
@@ -27,27 +30,38 @@ export default class EditChannelHeaderModal extends React.Component {
}
}
+ componentWillReceiveProps(nextProps) {
+ if (this.props.channel.header !== nextProps.channel.header) {
+ this.setState({
+ header: nextProps.channel.header
+ });
+ }
+ }
+
componentDidUpdate(prevProps) {
if (this.props.show && !prevProps.show) {
this.onShow();
}
}
- handleEdit() {
- var data = {};
- data.channel_id = this.props.channel.id;
-
- if (data.channel_id.length !== 26) {
- return;
- }
-
- data.channel_header = ReactDOM.findDOMNode(this.refs.textarea).value;
+ handleChange(e) {
+ this.setState({
+ header: e.target.value
+ });
+ }
- Client.updateChannelHeader(data,
- () => {
+ handleSubmit() {
+ Client.updateChannelHeader(
+ this.props.channel.id,
+ this.state.header,
+ (channel) => {
this.setState({serverError: ''});
- AsyncClient.getChannel(this.props.channel.id);
this.onHide();
+
+ AppDispatcher.handleServerAction({
+ type: Constants.ActionTypes.RECIEVED_CHANNEL,
+ channel
+ });
},
(err) => {
if (err.message === 'Invalid channel_header parameter') {
@@ -66,7 +80,8 @@ export default class EditChannelHeaderModal extends React.Component {
onHide() {
this.setState({
- serverError: ''
+ serverError: '',
+ header: this.props.channel.header
});
this.props.onHide();
@@ -94,7 +109,8 @@ export default class EditChannelHeaderModal extends React.Component {
rows='6'
id='edit_header'
maxLength='1024'
- defaultValue={this.props.channel.header}
+ value={this.state.header}
+ onChange={this.handleChange}
/>
{serverError}
</Modal.Body>
@@ -102,14 +118,14 @@ export default class EditChannelHeaderModal extends React.Component {
<button
type='button'
className='btn btn-default'
- onClick={this.props.onHide}
+ onClick={this.onHide}
>
{'Cancel'}
</button>
<button
type='button'
className='btn btn-primary'
- onClick={this.handleEdit}
+ onClick={this.handleSubmit}
>
{'Save'}
</button>
diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx
index 649ec7321..56bc00a7e 100644
--- a/web/react/components/invite_member_modal.jsx
+++ b/web/react/components/invite_member_modal.jsx
@@ -33,6 +33,7 @@ export default class InviteMemberModal extends React.Component {
firstNameErrors: {},
lastNameErrors: {},
emailEnabled: global.window.mm_config.SendEmailNotifications === 'true',
+ userCreationEnabled: global.window.mm_config.EnableUserCreation === 'true',
showConfirmModal: false,
isSendingEmails: false
};
@@ -252,7 +253,7 @@ export default class InviteMemberModal extends React.Component {
ref={'first_name' + index}
placeholder='First name'
maxLength='64'
- disabled={!this.state.emailEnabled}
+ disabled={!this.state.emailEnabled || !this.state.userCreationEnabled}
spellCheck='false'
/>
{firstNameError}
@@ -266,7 +267,7 @@ export default class InviteMemberModal extends React.Component {
ref={'last_name' + index}
placeholder='Last name'
maxLength='64'
- disabled={!this.state.emailEnabled}
+ disabled={!this.state.emailEnabled || !this.state.userCreationEnabled}
spellCheck='false'
/>
{lastNameError}
@@ -285,7 +286,7 @@ export default class InviteMemberModal extends React.Component {
className='form-control'
placeholder='email@domain.com'
maxLength='64'
- disabled={!this.state.emailEnabled}
+ disabled={!this.state.emailEnabled || !this.state.userCreationEnabled}
spellCheck='false'
/>
{emailError}
@@ -303,7 +304,7 @@ export default class InviteMemberModal extends React.Component {
var content = null;
var sendButton = null;
- if (this.state.emailEnabled) {
+ if (this.state.emailEnabled && this.state.userCreationEnabled) {
content = (
<div>
{serverError}
@@ -337,7 +338,7 @@ export default class InviteMemberModal extends React.Component {
{sendButtonLabel}
</button>
);
- } else {
+ } else if (this.state.userCreationEnabled) {
var teamInviteLink = null;
if (currentUser && TeamStore.getCurrent().type === 'O') {
var link = (
@@ -358,10 +359,16 @@ export default class InviteMemberModal extends React.Component {
content = (
<div>
- <p>Email is currently disabled for your team, and email invitations cannot be sent. Contact your system administrator to enable email and email invitations.</p>
+ <p>{'Email is currently disabled for your team, and email invitations cannot be sent. Contact your system administrator to enable email and email invitations.'}</p>
{teamInviteLink}
</div>
);
+ } else {
+ content = (
+ <div>
+ <p>{'User creation has been disabled for your team. Please ask your team administrator for details.'}</p>
+ </div>
+ );
}
return (
diff --git a/web/react/components/post_attachment_oembed.jsx b/web/react/components/post_attachment_oembed.jsx
index 13c32f744..4b12ee95e 100644
--- a/web/react/components/post_attachment_oembed.jsx
+++ b/web/react/components/post_attachment_oembed.jsx
@@ -14,7 +14,14 @@ export default class PostAttachmentOEmbed extends React.Component {
}
componentWillReceiveProps(nextProps) {
- this.fetchData(nextProps.link);
+ if (nextProps.link !== this.props.link) {
+ this.isLoading = false;
+ this.fetchData(nextProps.link);
+ }
+ }
+
+ componentDidMount() {
+ this.fetchData(this.props.link);
}
fetchData(link) {
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index 35a7727e9..dcbe56399 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -214,14 +214,14 @@ export default class PostBody extends React.Component {
}
createYoutubeEmbed(link) {
- const ytRegex = /.*(?:youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|watch\?(?:[a-zA-Z-_]+=[a-zA-Z0-9-_]+&)+v=)([^#\&\?]*).*/;
+ const ytRegex = /(?:http|https):\/\/(?:www\.)?(?:(?:youtube\.com\/(?:(?:v\/)|(\/u\/\w\/)|(?:(?:watch|embed\/watch)(?:\/|.*v=))|(?:embed\/)|(?:user\/[^\/]+\/u\/[0-9]\/)))|(?:youtu\.be\/))([^#\&\?]*)/;
const match = link.trim().match(ytRegex);
- if (!match || match[1].length !== 11) {
+ if (!match || match[2].length !== 11) {
return null;
}
- const youtubeId = match[1];
+ const youtubeId = match[2];
const time = this.handleYoutubeTime(link);
function onClick(e) {
diff --git a/web/react/components/providers.json b/web/react/components/providers.json
index 33e377a39..b5899c225 100644
--- a/web/react/components/providers.json
+++ b/web/react/components/providers.json
@@ -11,7 +11,7 @@
"https?://soundcloud.com/.*/.*"
],
"name": "SoundCloud",
- "height": 110
+ "height": 140
},
{
"patterns": [
@@ -349,7 +349,7 @@
"https?://vine.co/v/[a-zA-Z0-9]+"
],
"name": "Vine",
- "height": 110
+ "height": 490
},
{
"patterns": [
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index 3d7f449d1..8393440cb 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -71,49 +71,47 @@ export default class Sidebar extends React.Component {
getStateFromStores() {
const members = ChannelStore.getAllMembers();
const currentChannelId = ChannelStore.getCurrentId();
+ const currentUserId = UserStore.getCurrentId();
const channels = Object.assign([], ChannelStore.getAll());
channels.sort((a, b) => a.display_name.localeCompare(b.display_name));
const publicChannels = channels.filter((channel) => channel.type === Constants.OPEN_CHANNEL);
const privateChannels = channels.filter((channel) => channel.type === Constants.PRIVATE_CHANNEL);
- const directChannels = channels.filter((channel) => channel.type === Constants.DM_CHANNEL);
const preferences = PreferenceStore.getPreferences(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
- var visibleDirectChannels = [];
- for (var i = 0; i < directChannels.length; i++) {
- const dm = directChannels[i];
- const teammate = Utils.getDirectTeammate(dm.id);
- if (!teammate) {
+ const directChannels = [];
+ for (const preference of preferences) {
+ if (preference.value !== 'true') {
continue;
}
- const member = members[dm.id];
- const msgCount = dm.total_msg_count - member.msg_count;
+ const teammateId = preference.name;
- // always show a channel if either it is the current one or if it is unread, but it is not currently being left
- const forceShow = (currentChannelId === dm.id || msgCount > 0) && !this.isLeaving.get(dm.id);
- const preferenceShow = preferences.some((preference) => (preference.name === teammate.id && preference.value !== 'false'));
+ let directChannel = channels.find(Utils.isDirectChannelForUser.bind(null, teammateId));
- if (preferenceShow || forceShow) {
- dm.display_name = Utils.displayUsername(teammate.id);
- dm.teammate_id = teammate.id;
- dm.status = UserStore.getStatus(teammate.id);
+ // a direct channel doesn't exist yet so create a fake one
+ if (!directChannel) {
+ directChannel = {
+ name: Utils.getDirectChannelName(currentUserId, teammateId),
+ last_post_at: 0,
+ total_msg_count: 0,
+ type: Constants.DM_CHANNEL,
+ fake: true
+ };
+ }
- visibleDirectChannels.push(dm);
+ directChannel.display_name = Utils.displayUsername(teammateId);
+ directChannel.teammate_id = teammateId;
+ directChannel.status = UserStore.getStatus(teammateId);
- if (forceShow && !preferenceShow) {
- // make sure that unread direct channels are visible
- const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, teammate.id, 'true');
- AsyncClient.savePreferences([preference]);
- }
- }
+ directChannels.push(directChannel);
}
- const hiddenDirectChannelCount = UserStore.getActiveOnlyProfileList(true).length - visibleDirectChannels.length;
+ directChannels.sort(this.sortChannelsByDisplayName);
- visibleDirectChannels.sort(this.sortChannelsByDisplayName);
+ const hiddenDirectChannelCount = UserStore.getActiveOnlyProfileList(true).length - directChannels.length;
const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'});
@@ -122,7 +120,7 @@ export default class Sidebar extends React.Component {
members,
publicChannels,
privateChannels,
- visibleDirectChannels,
+ directChannels,
hiddenDirectChannelCount,
unreadCounts: JSON.parse(JSON.stringify(ChannelStore.getUnreadCounts())),
showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.CHANNEL_POPOVER
@@ -484,7 +482,7 @@ export default class Sidebar extends React.Component {
const privateChannelItems = this.state.privateChannels.map(this.createChannelElement);
- const directMessageItems = this.state.visibleDirectChannels.map((channel, index, arr) => {
+ const directMessageItems = this.state.directChannels.map((channel, index, arr) => {
return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel);
});
diff --git a/web/react/components/team_general_tab.jsx b/web/react/components/team_general_tab.jsx
index 03715d585..dc615f2e8 100644
--- a/web/react/components/team_general_tab.jsx
+++ b/web/react/components/team_general_tab.jsx
@@ -12,6 +12,7 @@ export default class GeneralTab extends React.Component {
constructor(props) {
super(props);
+ this.updateSection = this.updateSection.bind(this);
this.handleNameSubmit = this.handleNameSubmit.bind(this);
this.handleInviteIdSubmit = this.handleInviteIdSubmit.bind(this);
this.handleOpenInviteSubmit = this.handleOpenInviteSubmit.bind(this);
@@ -27,11 +28,22 @@ export default class GeneralTab extends React.Component {
this.handleTeamListingRadio = this.handleTeamListingRadio.bind(this);
this.handleGenerateInviteId = this.handleGenerateInviteId.bind(this);
- this.state = {
- name: props.team.display_name,
- invite_id: props.team.invite_id,
- allow_open_invite: props.team.allow_open_invite,
- allow_team_listing: props.team.allow_team_listing,
+ this.state = this.setupInitialState(props);
+ }
+
+ updateSection(section) {
+ this.setState(this.setupInitialState(this.props));
+ this.props.updateSection(section);
+ }
+
+ setupInitialState(props) {
+ const team = props.team;
+
+ return {
+ name: team.display_name,
+ invite_id: team.invite_id,
+ allow_open_invite: team.allow_open_invite,
+ allow_team_listing: team.allow_team_listing,
serverError: '',
clientError: ''
};
@@ -71,7 +83,7 @@ export default class GeneralTab extends React.Component {
(team) => {
TeamStore.saveTeam(team);
TeamStore.emitChange();
- this.props.updateSection('');
+ this.updateSection('');
},
(err) => {
state.serverError = err.message;
@@ -91,7 +103,7 @@ export default class GeneralTab extends React.Component {
(team) => {
TeamStore.saveTeam(team);
TeamStore.emitChange();
- this.props.updateSection('');
+ this.updateSection('');
},
(err) => {
state.serverError = err.message;
@@ -129,7 +141,7 @@ export default class GeneralTab extends React.Component {
(team) => {
TeamStore.saveTeam(team);
TeamStore.emitChange();
- this.props.updateSection('');
+ this.updateSection('');
},
(err) => {
state.serverError = err.message;
@@ -164,7 +176,7 @@ export default class GeneralTab extends React.Component {
(team) => {
TeamStore.saveTeam(team);
TeamStore.emitChange();
- this.props.updateSection('');
+ this.updateSection('');
},
(err) => {
state.serverError = err.message;
@@ -180,8 +192,7 @@ export default class GeneralTab extends React.Component {
}
handleClose() {
- this.setState({clientError: '', serverError: ''});
- this.props.updateSection('');
+ this.updateSection('');
}
componentDidMount() {
@@ -195,36 +206,36 @@ export default class GeneralTab extends React.Component {
onUpdateNameSection(e) {
e.preventDefault();
if (this.props.activeSection === 'name') {
- this.props.updateSection('');
+ this.updateSection('');
} else {
- this.props.updateSection('name');
+ this.updateSection('name');
}
}
onUpdateInviteIdSection(e) {
e.preventDefault();
if (this.props.activeSection === 'invite_id') {
- this.props.updateSection('');
+ this.updateSection('');
} else {
- this.props.updateSection('invite_id');
+ this.updateSection('invite_id');
}
}
onUpdateOpenInviteSection(e) {
e.preventDefault();
if (this.props.activeSection === 'open_invite') {
- this.props.updateSection('');
+ this.updateSection('');
} else {
- this.props.updateSection('open_invite');
+ this.updateSection('open_invite');
}
}
onUpdateTeamListingSection(e) {
e.preventDefault();
if (this.props.activeSection === 'team_listing') {
- this.props.updateSection('');
+ this.updateSection('');
} else {
- this.props.updateSection('team_listing');
+ this.updateSection('team_listing');
}
}
@@ -248,44 +259,59 @@ export default class GeneralTab extends React.Component {
serverError = this.state.serverError;
}
+ const enableTeamListing = global.window.mm_config.EnableTeamListing === 'true';
+
let teamListingSection;
if (this.props.activeSection === 'team_listing') {
- const inputs = [
- <div key='userTeamListingOptions'>
- <div className='radio'>
- <label>
- <input
- name='userTeamListingOptions'
- type='radio'
- defaultChecked={this.state.allow_team_listing}
- onChange={this.handleTeamListingRadio.bind(this, true)}
- />
- {'Yes'}
- </label>
- <br/>
+ const inputs = [];
+ let submitHandle = null;
+
+ if (enableTeamListing) {
+ submitHandle = this.handleTeamListingSubmit;
+
+ inputs.push(
+ <div key='userTeamListingOptions'>
+ <div className='radio'>
+ <label>
+ <input
+ name='userTeamListingOptions'
+ type='radio'
+ defaultChecked={this.state.allow_team_listing}
+ onChange={this.handleTeamListingRadio.bind(this, true)}
+ />
+ {'Yes'}
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ ref='teamListingRadioNo'
+ name='userTeamListingOptions'
+ type='radio'
+ defaultChecked={!this.state.allow_team_listing}
+ onChange={this.handleTeamListingRadio.bind(this, false)}
+ />
+ {'No'}
+ </label>
+ <br/>
+ </div>
+ <div><br/>{'Including this team will display the team name from the Team Directory section of the Home Page, and provide a link to the sign-in page.'}</div>
</div>
- <div className='radio'>
- <label>
- <input
- ref='teamListingRadioNo'
- name='userTeamListingOptions'
- type='radio'
- defaultChecked={!this.state.allow_team_listing}
- onChange={this.handleTeamListingRadio.bind(this, false)}
- />
- {'No'}
- </label>
- <br/>
+ );
+ } else {
+ inputs.push(
+ <div key='userTeamListingOptions'>
+ <div><br/>{'Contact your system administrator to turn on the team directory on the system home page.'}</div>
</div>
- <div><br/>{'Including this team will display the team name from the Team Directory section of the Home Page, and provide a link to the sign-in page.'}</div>
- </div>
- ];
+ );
+ }
teamListingSection = (
<SettingItemMax
title='Include this team in the Team Directory'
inputs={inputs}
- submit={this.handleTeamListingSubmit}
+ submit={submitHandle}
server_error={serverError}
client_error={clientError}
updateSection={this.onUpdateTeamListingSection}
@@ -293,10 +319,15 @@ export default class GeneralTab extends React.Component {
);
} else {
let describe = '';
- if (this.state.allow_team_listing === true) {
- describe = 'Yes';
+
+ if (enableTeamListing) {
+ if (this.state.allow_team_listing === true) {
+ describe = 'Yes';
+ } else {
+ describe = 'No';
+ }
} else {
- describe = 'No';
+ describe = 'Team directory is turned off for this system.';
}
teamListingSection = (
diff --git a/web/react/components/user_settings/manage_outgoing_hooks.jsx b/web/react/components/user_settings/manage_outgoing_hooks.jsx
index 9c0fe3709..fdbac9831 100644
--- a/web/react/components/user_settings/manage_outgoing_hooks.jsx
+++ b/web/react/components/user_settings/manage_outgoing_hooks.jsx
@@ -1,4 +1,4 @@
-// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import LoadingScreen from '../loading_screen.jsx';
diff --git a/web/react/dispatcher/event_helpers.jsx b/web/react/dispatcher/event_helpers.jsx
index bc1132765..297367ce9 100644
--- a/web/react/dispatcher/event_helpers.jsx
+++ b/web/react/dispatcher/event_helpers.jsx
@@ -173,3 +173,10 @@ export function emitClearSuggestions(suggestionId) {
id: suggestionId
});
}
+
+export function emitPreferenceChangedEvent(preference) {
+ AppDispatcher.handleServerAction({
+ type: Constants.ActionTypes.RECIEVED_PREFERENCE,
+ preference
+ });
+}
diff --git a/web/react/stores/channel_store.jsx b/web/react/stores/channel_store.jsx
index 0bfde77b4..afc960fcf 100644
--- a/web/react/stores/channel_store.jsx
+++ b/web/react/stores/channel_store.jsx
@@ -317,7 +317,9 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => {
case ActionTypes.RECIEVED_CHANNEL:
ChannelStore.pStoreChannel(action.channel);
- ChannelStore.pStoreChannelMember(action.member);
+ if (action.member) {
+ ChannelStore.pStoreChannelMember(action.member);
+ }
currentId = ChannelStore.getCurrentId();
if (currentId) {
ChannelStore.resetCounts(currentId);
diff --git a/web/react/stores/preference_store.jsx b/web/react/stores/preference_store.jsx
index 068bc29c2..543129aca 100644
--- a/web/react/stores/preference_store.jsx
+++ b/web/react/stores/preference_store.jsx
@@ -1,4 +1,4 @@
-// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import Constants from '../utils/constants.jsx';
@@ -90,8 +90,8 @@ class PreferenceStoreClass extends EventEmitter {
return preference;
}
- emitChange(preferences) {
- this.emit(CHANGE_EVENT, preferences);
+ emitChange() {
+ this.emit(CHANGE_EVENT);
}
addChangeListener(callback) {
@@ -106,6 +106,12 @@ class PreferenceStoreClass extends EventEmitter {
const action = payload.action;
switch (action.type) {
+ case ActionTypes.RECIEVED_PREFERENCE: {
+ const preference = action.preference;
+ this.setPreference(preference.category, preference.name, preference.value);
+ this.emitChange();
+ break;
+ }
case ActionTypes.RECIEVED_PREFERENCES: {
const preferences = this.getAllPreferences();
@@ -114,7 +120,7 @@ class PreferenceStoreClass extends EventEmitter {
}
this.setAllPreferences(preferences);
- this.emitChange(preferences);
+ this.emitChange();
break;
}
}
diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx
index d5aed40cf..24fa79ca6 100644
--- a/web/react/stores/socket_store.jsx
+++ b/web/react/stores/socket_store.jsx
@@ -136,6 +136,10 @@ class SocketStoreClass extends EventEmitter {
handleChannelViewedEvent(msg);
break;
+ case SocketEvents.PREFERENCE_CHANGED:
+ handlePreferenceChangedEvent(msg);
+ break;
+
default:
}
}
@@ -281,6 +285,11 @@ function handleChannelViewedEvent(msg) {
}
}
+function handlePreferenceChangedEvent(msg) {
+ const preference = JSON.parse(msg.props.preference);
+ EventHelpers.emitPreferenceChangedEvent(preference);
+}
+
var SocketStore = new SocketStoreClass();
/*SocketStore.dispatchToken = AppDispatcher.register((payload) => {
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index 09e962161..5d02a8c88 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -590,7 +590,12 @@ export function updateChannel(channel, success, error) {
track('api', 'api_channels_update');
}
-export function updateChannelHeader(data, success, error) {
+export function updateChannelHeader(channelId, header, success, error) {
+ const data = {
+ channel_id: channelId,
+ channel_header: header
+ };
+
$.ajax({
url: '/api/v1/channels/update_header',
dataType: 'json',
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 891bc4624..d23c18b5d 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -35,6 +35,7 @@ export default {
RECIEVED_AUDITS: null,
RECIEVED_TEAMS: null,
RECIEVED_STATUSES: null,
+ RECIEVED_PREFERENCE: null,
RECIEVED_PREFERENCES: null,
RECIEVED_MSG: null,
@@ -74,7 +75,8 @@ export default {
NEW_USER: 'new_user',
USER_ADDED: 'user_added',
USER_REMOVED: 'user_removed',
- TYPING: 'typing'
+ TYPING: 'typing',
+ PREFERENCE_CHANGED: 'preference_changed'
},
//SPECIAL_MENTIONS: ['all', 'channel'],
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index eb1d36ced..fb8b89252 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -300,7 +300,7 @@ export function extractLinks(text) {
Autolinker.link(text, {
replaceFn,
- urls: true,
+ urls: {schemeMatches: true, wwwMatches: true, tldMatches: false},
emails: false,
twitter: false,
phone: false,
@@ -1138,6 +1138,11 @@ export function getUserIdFromChannelName(channel) {
return otherUserId;
}
+// Returns true if the given channel is a direct channel between the current user and the given one
+export function isDirectChannelForUser(otherUserId, channel) {
+ return channel.type === Constants.DM_CHANNEL && getUserIdFromChannelName(channel) === otherUserId;
+}
+
export function importSlack(file, success, error) {
var formData = new FormData();
formData.append('file', file, file.name);
@@ -1253,4 +1258,4 @@ export function isFeatureEnabled(feature) {
export function isSystemMessage(post) {
return post.type && (post.type.lastIndexOf(Constants.SYSTEM_MESSAGE_PREFIX) === 0);
-} \ No newline at end of file
+}
diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss
index 9279cf0c4..a082ffb22 100644
--- a/web/sass-files/sass/partials/_modal.scss
+++ b/web/sass-files/sass/partials/_modal.scss
@@ -394,7 +394,6 @@
.modal-body {
padding: 10px 0 20px;
- @include clearfix;
}
.filter-row {
diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss
index f078cafb4..473ffb28d 100644
--- a/web/sass-files/sass/partials/_settings.scss
+++ b/web/sass-files/sass/partials/_settings.scss
@@ -183,7 +183,7 @@
text-align: right;
margin-bottom: 5px;
.fa {
- margin-right: 7px;
+ margin-right: 5px;
font-size: 12px;
@include opacity(0.5);
display: none;
@@ -335,6 +335,8 @@
.fa {
margin-right: 5px;
+ @include opacity(0.5);
+ font-size: 12px;
}
.member-menu {