summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile10
-rw-r--r--api/admin.go24
-rw-r--r--api/channel.go56
-rw-r--r--api/channel_benchmark_test.go11
-rw-r--r--api/channel_test.go80
-rw-r--r--api/post.go2
-rw-r--r--doc/README.md2
-rw-r--r--doc/import/slack-import.md18
-rw-r--r--doc/install/prod-ubuntu.md17
-rw-r--r--mattermost.go2
-rw-r--r--model/channel_member.go58
-rw-r--r--model/channel_member_test.go18
-rw-r--r--model/client.go4
-rw-r--r--model/post.go5
-rw-r--r--model/post_test.go13
-rw-r--r--store/sql_channel_store.go102
-rw-r--r--store/sql_channel_store_test.go32
-rw-r--r--store/sql_post_store_test.go2
-rw-r--r--store/store.go2
-rw-r--r--web/react/components/admin_console/sql_settings.jsx6
-rw-r--r--web/react/components/channel_notifications.jsx289
-rw-r--r--web/react/components/notify_counts.jsx2
-rw-r--r--web/react/components/post_info.jsx2
-rw-r--r--web/react/components/sidebar.jsx16
-rw-r--r--web/react/components/user_settings/manage_incoming_hooks.jsx2
-rw-r--r--web/react/components/user_settings/user_settings_appearance.jsx15
-rw-r--r--web/react/components/user_settings/user_settings_notifications.jsx2
-rw-r--r--web/react/utils/async_client.jsx16
-rw-r--r--web/react/utils/client.jsx20
-rw-r--r--web/react/utils/text_formatting.jsx6
-rw-r--r--web/templates/head.html19
31 files changed, 572 insertions, 281 deletions
diff --git a/Makefile b/Makefile
index 5cb5467f6..88df59d8b 100644
--- a/Makefile
+++ b/Makefile
@@ -140,11 +140,11 @@ check: install
test: install
@mkdir -p logs
- @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=180s ./api || exit 1
- @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=12s ./model || exit 1
- @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./store || exit 1
- @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./utils || exit 1
- @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./web || exit 1
+ @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=600s ./api || exit 1
+ @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=60s ./model || exit 1
+ @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=600s ./store || exit 1
+ @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=600s ./utils || exit 1
+ @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=600s ./web || exit 1
benchmark: install
@mkdir -p logs
diff --git a/api/admin.go b/api/admin.go
index 3ef8c12a8..d9714d6d2 100644
--- a/api/admin.go
+++ b/api/admin.go
@@ -23,8 +23,10 @@ func InitAdmin(r *mux.Router) {
sr.Handle("/logs", ApiUserRequired(getLogs)).Methods("GET")
sr.Handle("/config", ApiUserRequired(getConfig)).Methods("GET")
sr.Handle("/save_config", ApiUserRequired(saveConfig)).Methods("POST")
- sr.Handle("/client_props", ApiAppHandler(getClientProperties)).Methods("GET")
sr.Handle("/test_email", ApiUserRequired(testEmail)).Methods("POST")
+ sr.Handle("/client_props", ApiAppHandler(getClientProperties)).Methods("GET")
+ sr.Handle("/log_client", ApiAppHandler(logClient)).Methods("POST")
+
}
func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -59,6 +61,26 @@ func getClientProperties(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJson(utils.ClientProperties)))
}
+func logClient(c *Context, w http.ResponseWriter, r *http.Request) {
+ m := model.MapFromJson(r.Body)
+
+ lvl := m["level"]
+ msg := m["message"]
+
+ if len(msg) > 400 {
+ msg = msg[0:399]
+ }
+
+ if lvl == "ERROR" {
+ err := model.NewAppError("client", msg, "")
+ c.LogError(err)
+ }
+
+ rm := make(map[string]string)
+ rm["SUCCESS"] = "true"
+ w.Write([]byte(model.MapToJson(rm)))
+}
+
func getConfig(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.HasSystemAdminPermissions("getConfig") {
return
diff --git a/api/channel.go b/api/channel.go
index 896e22793..6494e3528 100644
--- a/api/channel.go
+++ b/api/channel.go
@@ -23,7 +23,7 @@ func InitChannel(r *mux.Router) {
sr.Handle("/create_direct", ApiUserRequired(createDirectChannel)).Methods("POST")
sr.Handle("/update", ApiUserRequired(updateChannel)).Methods("POST")
sr.Handle("/update_desc", ApiUserRequired(updateChannelDesc)).Methods("POST")
- sr.Handle("/update_notify_level", ApiUserRequired(updateNotifyLevel)).Methods("POST")
+ sr.Handle("/update_notify_props", ApiUserRequired(updateNotifyProps)).Methods("POST")
sr.Handle("/{id:[A-Za-z0-9]+}/", ApiUserRequiredActivity(getChannel, false)).Methods("GET")
sr.Handle("/{id:[A-Za-z0-9]+}/extra_info", ApiUserRequired(getChannelExtraInfo)).Methods("GET")
sr.Handle("/{id:[A-Za-z0-9]+}/join", ApiUserRequired(joinChannel)).Methods("POST")
@@ -76,7 +76,7 @@ func CreateChannel(c *Context, channel *model.Channel, addMember bool) (*model.C
if addMember {
cm := &model.ChannelMember{ChannelId: sc.Id, UserId: c.Session.UserId,
- Roles: model.CHANNEL_ROLE_ADMIN, NotifyLevel: model.CHANNEL_NOTIFY_ALL}
+ Roles: model.CHANNEL_ROLE_ADMIN, NotifyProps: model.GetDefaultChannelNotifyProps()}
if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil {
return nil, cmresult.Err
@@ -134,8 +134,7 @@ func CreateDirectChannel(c *Context, otherUserId string) (*model.Channel, *model
if sc, err := CreateChannel(c, channel, true); err != nil {
return nil, err
} else {
- cm := &model.ChannelMember{ChannelId: sc.Id, UserId: otherUserId,
- Roles: "", NotifyLevel: model.CHANNEL_NOTIFY_ALL}
+ cm := &model.ChannelMember{ChannelId: sc.Id, UserId: otherUserId, Roles: "", NotifyProps: model.GetDefaultChannelNotifyProps()}
if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil {
return nil, cmresult.Err
@@ -372,7 +371,8 @@ func JoinChannel(c *Context, channelId string, role string) {
}
if channel.Type == model.CHANNEL_OPEN {
- cm := &model.ChannelMember{ChannelId: channel.Id, UserId: c.Session.UserId, NotifyLevel: model.CHANNEL_NOTIFY_ALL, Roles: role}
+ cm := &model.ChannelMember{ChannelId: channel.Id, UserId: c.Session.UserId,
+ Roles: role, NotifyProps: model.GetDefaultChannelNotifyProps()}
if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil {
c.Err = cmresult.Err
@@ -405,7 +405,9 @@ func JoinDefaultChannels(user *model.User, channelRole string) *model.AppError {
if result := <-Srv.Store.Channel().GetByName(user.TeamId, "town-square"); result.Err != nil {
err = result.Err
} else {
- cm := &model.ChannelMember{ChannelId: result.Data.(*model.Channel).Id, UserId: user.Id, NotifyLevel: model.CHANNEL_NOTIFY_ALL, Roles: channelRole}
+ cm := &model.ChannelMember{ChannelId: result.Data.(*model.Channel).Id, UserId: user.Id,
+ Roles: channelRole, NotifyProps: model.GetDefaultChannelNotifyProps()}
+
if cmResult := <-Srv.Store.Channel().SaveMember(cm); cmResult.Err != nil {
err = cmResult.Err
}
@@ -414,7 +416,9 @@ func JoinDefaultChannels(user *model.User, channelRole string) *model.AppError {
if result := <-Srv.Store.Channel().GetByName(user.TeamId, "off-topic"); result.Err != nil {
err = result.Err
} else {
- cm := &model.ChannelMember{ChannelId: result.Data.(*model.Channel).Id, UserId: user.Id, NotifyLevel: model.CHANNEL_NOTIFY_ALL, Roles: channelRole}
+ cm := &model.ChannelMember{ChannelId: result.Data.(*model.Channel).Id, UserId: user.Id,
+ Roles: channelRole, NotifyProps: model.GetDefaultChannelNotifyProps()}
+
if cmResult := <-Srv.Store.Channel().SaveMember(cm); cmResult.Err != nil {
err = cmResult.Err
}
@@ -694,7 +698,7 @@ func addChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
} else {
oUser := oresult.Data.(*model.User)
- cm := &model.ChannelMember{ChannelId: channel.Id, UserId: userId, NotifyLevel: model.CHANNEL_NOTIFY_ALL}
+ cm := &model.ChannelMember{ChannelId: channel.Id, UserId: userId, NotifyProps: model.GetDefaultChannelNotifyProps()}
if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil {
l4g.Error("Failed to add member user_id=%v channel_id=%v err=%v", userId, id, cmresult.Err)
@@ -784,23 +788,18 @@ func removeChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
}
-func updateNotifyLevel(c *Context, w http.ResponseWriter, r *http.Request) {
+func updateNotifyProps(c *Context, w http.ResponseWriter, r *http.Request) {
data := model.MapFromJson(r.Body)
+
userId := data["user_id"]
if len(userId) != 26 {
- c.SetInvalidParam("updateNotifyLevel", "user_id")
+ c.SetInvalidParam("updateMarkUnreadLevel", "user_id")
return
}
channelId := data["channel_id"]
if len(channelId) != 26 {
- c.SetInvalidParam("updateNotifyLevel", "channel_id")
- return
- }
-
- notifyLevel := data["notify_level"]
- if len(notifyLevel) == 0 || !model.IsChannelNotifyLevelValid(notifyLevel) {
- c.SetInvalidParam("updateNotifyLevel", "notify_level")
+ c.SetInvalidParam("updateMarkUnreadLevel", "channel_id")
return
}
@@ -814,10 +813,29 @@ func updateNotifyLevel(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if result := <-Srv.Store.Channel().UpdateNotifyLevel(channelId, userId, notifyLevel); result.Err != nil {
+ result := <-Srv.Store.Channel().GetMember(channelId, userId)
+ if result.Err != nil {
+ c.Err = result.Err
+ return
+ }
+
+ member := result.Data.(model.ChannelMember)
+
+ // update whichever notify properties have been provided, but don't change the others
+ if markUnread, exists := data["mark_unread"]; exists {
+ member.NotifyProps["mark_unread"] = markUnread
+ }
+
+ if desktop, exists := data["desktop"]; exists {
+ member.NotifyProps["desktop"] = desktop
+ }
+
+ if result := <-Srv.Store.Channel().UpdateMember(&member); result.Err != nil {
c.Err = result.Err
return
+ } else {
+ // return the updated notify properties including any unchanged ones
+ w.Write([]byte(model.MapToJson(member.NotifyProps)))
}
- w.Write([]byte(model.MapToJson(data)))
}
diff --git a/api/channel_benchmark_test.go b/api/channel_benchmark_test.go
index 77e679c14..7820f4a03 100644
--- a/api/channel_benchmark_test.go
+++ b/api/channel_benchmark_test.go
@@ -255,7 +255,7 @@ func BenchmarkRemoveChannelMember(b *testing.B) {
}
}
-func BenchmarkUpdateNotifyLevel(b *testing.B) {
+func BenchmarkUpdateNotifyProps(b *testing.B) {
var (
NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS}
)
@@ -271,9 +271,10 @@ func BenchmarkUpdateNotifyLevel(b *testing.B) {
for i := range data {
newmap := map[string]string{
- "channel_id": channels[i].Id,
- "user_id": user.Id,
- "notify_level": model.CHANNEL_NOTIFY_MENTION,
+ "channel_id": channels[i].Id,
+ "user_id": user.Id,
+ "desktop": model.CHANNEL_NOTIFY_MENTION,
+ "mark_unread": model.CHANNEL_MARK_UNREAD_MENTION,
}
data[i] = newmap
}
@@ -282,7 +283,7 @@ func BenchmarkUpdateNotifyLevel(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
for j := range channels {
- Client.Must(Client.UpdateNotifyLevel(data[j]))
+ Client.Must(Client.UpdateNotifyProps(data[j]))
}
}
}
diff --git a/api/channel_test.go b/api/channel_test.go
index 7845ac499..e6c7ed80e 100644
--- a/api/channel_test.go
+++ b/api/channel_test.go
@@ -803,7 +803,7 @@ func TestRemoveChannelMember(t *testing.T) {
}
-func TestUpdateNotifyLevel(t *testing.T) {
+func TestUpdateNotifyProps(t *testing.T) {
Setup()
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
@@ -821,55 +821,94 @@ func TestUpdateNotifyLevel(t *testing.T) {
data := make(map[string]string)
data["channel_id"] = channel1.Id
data["user_id"] = user.Id
- data["notify_level"] = model.CHANNEL_NOTIFY_MENTION
+ data["desktop"] = model.CHANNEL_NOTIFY_MENTION
timeBeforeUpdate := model.GetMillis()
time.Sleep(100 * time.Millisecond)
- if _, err := Client.UpdateNotifyLevel(data); err != nil {
+ // test updating desktop
+ if result, err := Client.UpdateNotifyProps(data); err != nil {
t.Fatal(err)
+ } else if notifyProps := result.Data.(map[string]string); notifyProps["desktop"] != model.CHANNEL_NOTIFY_MENTION {
+ t.Fatal("NotifyProps[\"desktop\"] did not update properly")
+ } else if notifyProps["mark_unread"] != model.CHANNEL_MARK_UNREAD_ALL {
+ t.Fatalf("NotifyProps[\"mark_unread\"] changed to %v", notifyProps["mark_unread"])
}
rget := Client.Must(Client.GetChannels(""))
rdata := rget.Data.(*model.ChannelList)
- if len(rdata.Members) == 0 || rdata.Members[channel1.Id].NotifyLevel != data["notify_level"] {
- t.Fatal("NotifyLevel did not update properly")
+ if len(rdata.Members) == 0 || rdata.Members[channel1.Id].NotifyProps["desktop"] != data["desktop"] {
+ t.Fatal("NotifyProps[\"desktop\"] did not update properly")
+ } else if rdata.Members[channel1.Id].LastUpdateAt <= timeBeforeUpdate {
+ t.Fatal("LastUpdateAt did not update")
}
- if rdata.Members[channel1.Id].LastUpdateAt <= timeBeforeUpdate {
- t.Fatal("LastUpdateAt did not update")
+ // test an empty update
+ delete(data, "desktop")
+
+ if result, err := Client.UpdateNotifyProps(data); err != nil {
+ t.Fatal(err)
+ } else if notifyProps := result.Data.(map[string]string); notifyProps["mark_unread"] != model.CHANNEL_MARK_UNREAD_ALL {
+ t.Fatalf("NotifyProps[\"mark_unread\"] changed to %v", notifyProps["mark_unread"])
+ } else if notifyProps["desktop"] != model.CHANNEL_NOTIFY_MENTION {
+ t.Fatalf("NotifyProps[\"desktop\"] changed to %v", notifyProps["desktop"])
}
+ // test updating mark unread
+ data["mark_unread"] = model.CHANNEL_MARK_UNREAD_MENTION
+
+ if result, err := Client.UpdateNotifyProps(data); err != nil {
+ t.Fatal(err)
+ } else if notifyProps := result.Data.(map[string]string); notifyProps["mark_unread"] != model.CHANNEL_MARK_UNREAD_MENTION {
+ t.Fatal("NotifyProps[\"mark_unread\"] did not update properly")
+ } else if notifyProps["desktop"] != model.CHANNEL_NOTIFY_MENTION {
+ t.Fatalf("NotifyProps[\"desktop\"] changed to %v", notifyProps["desktop"])
+ }
+
+ // test updating both
+ data["desktop"] = model.CHANNEL_NOTIFY_NONE
+ data["mark_unread"] = model.CHANNEL_MARK_UNREAD_MENTION
+
+ if result, err := Client.UpdateNotifyProps(data); err != nil {
+ t.Fatal(err)
+ } else if notifyProps := result.Data.(map[string]string); notifyProps["desktop"] != model.CHANNEL_NOTIFY_NONE {
+ t.Fatal("NotifyProps[\"desktop\"] did not update properly")
+ } else if notifyProps["mark_unread"] != model.CHANNEL_MARK_UNREAD_MENTION {
+ t.Fatal("NotifyProps[\"mark_unread\"] did not update properly")
+ }
+
+ // test error cases
data["user_id"] = "junk"
- if _, err := Client.UpdateNotifyLevel(data); err == nil {
+ if _, err := Client.UpdateNotifyProps(data); err == nil {
t.Fatal("Should have errored - bad user id")
}
data["user_id"] = "12345678901234567890123456"
- if _, err := Client.UpdateNotifyLevel(data); err == nil {
+ if _, err := Client.UpdateNotifyProps(data); err == nil {
t.Fatal("Should have errored - bad user id")
}
data["user_id"] = user.Id
data["channel_id"] = "junk"
- if _, err := Client.UpdateNotifyLevel(data); err == nil {
+ if _, err := Client.UpdateNotifyProps(data); err == nil {
t.Fatal("Should have errored - bad channel id")
}
data["channel_id"] = "12345678901234567890123456"
- if _, err := Client.UpdateNotifyLevel(data); err == nil {
+ if _, err := Client.UpdateNotifyProps(data); err == nil {
t.Fatal("Should have errored - bad channel id")
}
- data["channel_id"] = channel1.Id
- data["notify_level"] = ""
- if _, err := Client.UpdateNotifyLevel(data); err == nil {
- t.Fatal("Should have errored - empty notify level")
+ data["desktop"] = "junk"
+ data["mark_unread"] = model.CHANNEL_MARK_UNREAD_ALL
+ if _, err := Client.UpdateNotifyProps(data); err == nil {
+ t.Fatal("Should have errored - bad desktop notify level")
}
- data["notify_level"] = "junk"
- if _, err := Client.UpdateNotifyLevel(data); err == nil {
- t.Fatal("Should have errored - bad notify level")
+ data["desktop"] = model.CHANNEL_NOTIFY_ALL
+ data["mark_unread"] = "junk"
+ if _, err := Client.UpdateNotifyProps(data); err == nil {
+ t.Fatal("Should have errored - bad mark unread level")
}
user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
@@ -879,8 +918,9 @@ func TestUpdateNotifyLevel(t *testing.T) {
data["channel_id"] = channel1.Id
data["user_id"] = user2.Id
- data["notify_level"] = model.CHANNEL_NOTIFY_MENTION
- if _, err := Client.UpdateNotifyLevel(data); err == nil {
+ data["desktop"] = model.CHANNEL_NOTIFY_MENTION
+ data["mark_unread"] = model.CHANNEL_MARK_UNREAD_MENTION
+ if _, err := Client.UpdateNotifyProps(data); err == nil {
t.Fatal("Should have errored - user not in channel")
}
}
diff --git a/api/post.go b/api/post.go
index 2b683fb7d..65dad0eb6 100644
--- a/api/post.go
+++ b/api/post.go
@@ -88,6 +88,8 @@ func CreatePost(c *Context, post *model.Post, doUpdateLastViewed bool) (*model.P
}
}
+ post.CreateAt = 0
+
post.Hashtags, _ = model.ParseHashtags(post.Message)
post.UserId = c.Session.UserId
diff --git a/doc/README.md b/doc/README.md
index 7713c40ab..dc1591705 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -9,6 +9,7 @@
### Installation
- [Software and Hardware Requirements](install/requirements.md)
+- [Production Installation](install/prod-ubuntu.md)
- [Local Machine Setup ](install/single-container-install.md)
- [AWS Elastic Beanstalk Setup](install/aws-ebs-setup.md)
- [Developer Machine Setup](install/dev-setup.md)
@@ -27,3 +28,4 @@
## End User Help
- [Mattermost Markdown Formatting](help/enduser/markdown.md)
+- [Slack Import](https://github.com/mattermost/platform/blob/master/doc/import/slack-import.md)
diff --git a/doc/import/slack-import.md b/doc/import/slack-import.md
new file mode 100644
index 000000000..c30de0567
--- /dev/null
+++ b/doc/import/slack-import.md
@@ -0,0 +1,18 @@
+#### Slack Import (Preview)
+
+*Note: As a SaaS service, Slack is able to change its export format quickly. If you encounter issues not mentioned in the documentation below, please let us know by [filing an issue](https://github.com/mattermost/platform/issues).*
+
+The Slack Import feature in Mattermost is in "Preview" and focus is on supporting migration of teams of less than 100 registered users. The feature can be accessed from by Team Administrators and Team Owners via the `Team Settings -> Import` menu option.
+
+Mattermost currently supports the processing of an "Export" file from Slack containing account information and public channel archives from a Slack team.
+
+- In the feature preview, emails and usernames from Slack are used to create new Mattermost accounts, connected to messages history in imported Slack channels. Users can activate these accounts and by going to the Password Reset screen in Mattermost to set new credentials.
+- Once logged in, users will have access to previous Slack messages shared in public channels, now imported to Mattermost.
+
+Limitations:
+
+- Newly added markdown suppport in Slack's Posts 2.0 feature announced on September 28, 2015 is not yet supported.
+- Slack does not export files or images your team has stored in Slack's database. Mattermost will provide links to the location of your assets in Slack's web UI.
+- Slack does not export any content from private groups or direct messages that your team has stored in Slack's database.
+- The Preview release of Slack Import does not offer pre-checks or roll-back and will not import Slack accounts with username or email address collisions with existing Mattermost accounts. Also, Slack channel names with underscores will not import. Also, mentions do not yet resolve as Mattermost usernames (still show Slack ID).
+
diff --git a/doc/install/prod-ubuntu.md b/doc/install/prod-ubuntu.md
index 866b1bdbe..d2490062d 100644
--- a/doc/install/prod-ubuntu.md
+++ b/doc/install/prod-ubuntu.md
@@ -6,7 +6,7 @@
* ``` sudo apt-get update```
* ``` sudo apt-get upgrade```
-## Setup Database Server
+## Set up Database Server
1. For the purposes of this guide we will assume this server has an IP address of 10.10.10.1
1. Install PostgreSQL 9.3+ (or MySQL 5.2+)
* ``` sudo apt-get install postgresql postgresql-contrib```
@@ -25,13 +25,13 @@
1. You can exit the postgres account by typing:
* ``` exit```
-## Setup Mattermost Server
+## Set up Mattermost Server
1. For the purposes of this guide we will assume this server has an IP address of 10.10.10.2
1. Download the latest Mattermost Server by typing:
* ``` wget https://github.com/mattermost/platform/releases/download/v1.0.0/mattermost.tar.gz```
1. Unzip the Mattermost Server by typing:
* ``` tar -xvzf mattermost.tar.gz```
-1. For the sake of making this guide simple we located the files at `/home/ubuntu/mattermost`, in the future we will give guidance for storing under `/opt`.
+1. For the sake of making this guide simple we located the files at `/home/ubuntu/mattermost`. In the future we will give guidance for storing under `/opt`.
1. We have also elected to run the Mattermost Server as the `ubuntu` account for simplicity. We recommend settings up and running the service under a `mattermost` user account with limited permissions.
1. Create the storage directory for files. We assume you will have attached a large drive for storage of images and files. For this setup we will assume the directory is located at `/mattermost/data`.
* Create the directory by typing:
@@ -70,7 +70,7 @@ exec bin/platform
* You should see a page titles *Mattermost - Signup*
* You can also stop the process by running the command ` sudo stop mattermost`, but we will skip this step for now.
-## Setup Nginx Server
+## Set up Nginx Server
1. For the purposes of this guide we will assume this server has an IP address of 10.10.10.3
1. We use Nginx for proxying request to the Mattermost Server. The main benefits are:
* SSL termination
@@ -90,8 +90,7 @@ exec bin/platform
1. Configure Nginx to proxy connections from the internet to the Mattermost Server
* Create a configuration for Mattermost
* ``` sudo touch /etc/nginx/sites-available/mattermost```
- * Below is a sample configuration with the minimum settings required to configure Mattermost.
- *
+ * Below is a sample configuration with the minimum settings required to configure Mattermost
```
server {
server_name mattermost.example.com;
@@ -118,7 +117,7 @@ exec bin/platform
* ``` curl http://localhost```
* You should see a page titles *Mattermost - Signup*
-## Setup Nginx with SSL (Recommended)
+## Set up Nginx with SSL (Recommended)
1. You will need a SSL cert from a certificate authority.
1. For simplicity we will generate a test certificate.
* ``` mkdir ~/cert```
@@ -177,9 +176,9 @@ exec bin/platform
* Save the Settings
1. Update File Settings
* Change *Local Directory Location* from `./data/` to `/mattermost/data`
-1. Update Log Settings
+1. Update Log Settings.
* Set *Log to The Console* to false
-1. Update Rate Limit Settings
+1. Update Rate Limit Settings.
* Set *Vary By Remote Address* to false
* Set *Vary By HTTP Header* to X-Real-IP
1. Feel free to modify other settings.
diff --git a/mattermost.go b/mattermost.go
index ff7b0f3f3..94bbe344d 100644
--- a/mattermost.go
+++ b/mattermost.go
@@ -355,7 +355,7 @@ Usage:
-reset_password Resets the password for a user. It requires the
-team_name, -email and -password flag.
Example:
- platform -reset_password -team_name="name" -email="user@example.com" -paossword="newpassword"
+ platform -reset_password -team_name="name" -email="user@example.com" -password="newpassword"
`
diff --git a/model/channel_member.go b/model/channel_member.go
index 50f51304b..3ae612700 100644
--- a/model/channel_member.go
+++ b/model/channel_member.go
@@ -10,22 +10,24 @@ import (
)
const (
- CHANNEL_ROLE_ADMIN = "admin"
- CHANNEL_NOTIFY_ALL = "all"
- CHANNEL_NOTIFY_MENTION = "mention"
- CHANNEL_NOTIFY_NONE = "none"
- CHANNEL_NOTIFY_QUIET = "quiet"
+ CHANNEL_ROLE_ADMIN = "admin"
+ CHANNEL_NOTIFY_DEFAULT = "default"
+ CHANNEL_NOTIFY_ALL = "all"
+ CHANNEL_NOTIFY_MENTION = "mention"
+ CHANNEL_NOTIFY_NONE = "none"
+ CHANNEL_MARK_UNREAD_ALL = "all"
+ CHANNEL_MARK_UNREAD_MENTION = "mention"
)
type ChannelMember struct {
- ChannelId string `json:"channel_id"`
- UserId string `json:"user_id"`
- Roles string `json:"roles"`
- LastViewedAt int64 `json:"last_viewed_at"`
- MsgCount int64 `json:"msg_count"`
- MentionCount int64 `json:"mention_count"`
- NotifyLevel string `json:"notify_level"`
- LastUpdateAt int64 `json:"last_update_at"`
+ ChannelId string `json:"channel_id"`
+ UserId string `json:"user_id"`
+ Roles string `json:"roles"`
+ LastViewedAt int64 `json:"last_viewed_at"`
+ MsgCount int64 `json:"msg_count"`
+ MentionCount int64 `json:"mention_count"`
+ NotifyProps StringMap `json:"notify_props"`
+ LastUpdateAt int64 `json:"last_update_at"`
}
func (o *ChannelMember) ToJson() string {
@@ -64,8 +66,14 @@ func (o *ChannelMember) IsValid() *AppError {
}
}
- if len(o.NotifyLevel) > 20 || !IsChannelNotifyLevelValid(o.NotifyLevel) {
- return NewAppError("ChannelMember.IsValid", "Invalid notify level", "notify_level="+o.NotifyLevel)
+ notifyLevel := o.NotifyProps["desktop"]
+ if len(notifyLevel) > 20 || !IsChannelNotifyLevelValid(notifyLevel) {
+ return NewAppError("ChannelMember.IsValid", "Invalid notify level", "notify_level="+notifyLevel)
+ }
+
+ markUnreadLevel := o.NotifyProps["mark_unread"]
+ if len(markUnreadLevel) > 20 || !IsChannelMarkUnreadLevelValid(markUnreadLevel) {
+ return NewAppError("ChannelMember.IsValid", "Invalid mark unread level", "mark_unread_level="+markUnreadLevel)
}
return nil
@@ -75,6 +83,24 @@ func (o *ChannelMember) PreSave() {
o.LastUpdateAt = GetMillis()
}
+func (o *ChannelMember) PreUpdate() {
+ o.LastUpdateAt = GetMillis()
+}
+
func IsChannelNotifyLevelValid(notifyLevel string) bool {
- return notifyLevel == CHANNEL_NOTIFY_ALL || notifyLevel == CHANNEL_NOTIFY_MENTION || notifyLevel == CHANNEL_NOTIFY_NONE || notifyLevel == CHANNEL_NOTIFY_QUIET
+ return notifyLevel == CHANNEL_NOTIFY_DEFAULT ||
+ notifyLevel == CHANNEL_NOTIFY_ALL ||
+ notifyLevel == CHANNEL_NOTIFY_MENTION ||
+ notifyLevel == CHANNEL_NOTIFY_NONE
+}
+
+func IsChannelMarkUnreadLevelValid(markUnreadLevel string) bool {
+ return markUnreadLevel == CHANNEL_MARK_UNREAD_ALL || markUnreadLevel == CHANNEL_MARK_UNREAD_MENTION
+}
+
+func GetDefaultChannelNotifyProps() StringMap {
+ return StringMap{
+ "desktop": CHANNEL_NOTIFY_DEFAULT,
+ "mark_unread": CHANNEL_MARK_UNREAD_ALL,
+ }
}
diff --git a/model/channel_member_test.go b/model/channel_member_test.go
index 3b64ffbf7..edbb46e9b 100644
--- a/model/channel_member_test.go
+++ b/model/channel_member_test.go
@@ -31,24 +31,34 @@ func TestChannelMemberIsValid(t *testing.T) {
}
o.Roles = "missing"
- o.NotifyLevel = CHANNEL_NOTIFY_ALL
+ o.NotifyProps = GetDefaultChannelNotifyProps()
o.UserId = NewId()
if err := o.IsValid(); err == nil {
t.Fatal("should be invalid")
}
o.Roles = CHANNEL_ROLE_ADMIN
- o.NotifyLevel = "junk"
+ o.NotifyProps["desktop"] = "junk"
if err := o.IsValid(); err == nil {
t.Fatal("should be invalid")
}
- o.NotifyLevel = "123456789012345678901"
+ o.NotifyProps["desktop"] = "123456789012345678901"
if err := o.IsValid(); err == nil {
t.Fatal("should be invalid")
}
- o.NotifyLevel = CHANNEL_NOTIFY_ALL
+ o.NotifyProps["desktop"] = CHANNEL_NOTIFY_ALL
+ if err := o.IsValid(); err != nil {
+ t.Fatal(err)
+ }
+
+ o.NotifyProps["mark_unread"] = "123456789012345678901"
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.NotifyProps["mark_unread"] = CHANNEL_MARK_UNREAD_ALL
if err := o.IsValid(); err != nil {
t.Fatal(err)
}
diff --git a/model/client.go b/model/client.go
index 26e00864d..a291cc4f2 100644
--- a/model/client.go
+++ b/model/client.go
@@ -450,8 +450,8 @@ func (c *Client) UpdateChannelDesc(data map[string]string) (*Result, *AppError)
}
}
-func (c *Client) UpdateNotifyLevel(data map[string]string) (*Result, *AppError) {
- if r, err := c.DoApiPost("/channels/update_notify_level", MapToJson(data)); err != nil {
+func (c *Client) UpdateNotifyProps(data map[string]string) (*Result, *AppError) {
+ if r, err := c.DoApiPost("/channels/update_notify_props", MapToJson(data)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
diff --git a/model/post.go b/model/post.go
index e78469940..1fc5963c3 100644
--- a/model/post.go
+++ b/model/post.go
@@ -120,7 +120,10 @@ func (o *Post) PreSave() {
o.OriginalId = ""
- o.CreateAt = GetMillis()
+ if o.CreateAt == 0 {
+ o.CreateAt = GetMillis()
+ }
+
o.UpdateAt = o.CreateAt
if o.Props == nil {
diff --git a/model/post_test.go b/model/post_test.go
index 38f4b4c98..a6b880fa0 100644
--- a/model/post_test.go
+++ b/model/post_test.go
@@ -83,5 +83,18 @@ func TestPostIsValid(t *testing.T) {
func TestPostPreSave(t *testing.T) {
o := Post{Message: "test"}
o.PreSave()
+
+ if o.CreateAt == 0 {
+ t.Fatal("should be set")
+ }
+
+ past := GetMillis() - 1
+ o = Post{Message: "test", CreateAt: past}
+ o.PreSave()
+
+ if o.CreateAt > past {
+ t.Fatal("should not be updated")
+ }
+
o.Etag()
}
diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go
index cb686090e..3bbe7e716 100644
--- a/store/sql_channel_store.go
+++ b/store/sql_channel_store.go
@@ -4,6 +4,7 @@
package store
import (
+ l4g "code.google.com/p/log4go"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
)
@@ -30,13 +31,55 @@ func NewSqlChannelStore(sqlStore *SqlStore) ChannelStore {
tablem.ColMap("ChannelId").SetMaxSize(26)
tablem.ColMap("UserId").SetMaxSize(26)
tablem.ColMap("Roles").SetMaxSize(64)
- tablem.ColMap("NotifyLevel").SetMaxSize(20)
+ tablem.ColMap("NotifyProps").SetMaxSize(2000)
}
return s
}
func (s SqlChannelStore) UpgradeSchemaIfNeeded() {
+ if s.CreateColumnIfNotExists("ChannelMembers", "NotifyProps", "varchar(2000)", "varchar(2000)", "{}") {
+ // populate NotifyProps from existing NotifyLevel field
+
+ // set default values
+ _, err := s.GetMaster().Exec(
+ `UPDATE
+ ChannelMembers
+ SET
+ NotifyProps = CONCAT('{"desktop":"', CONCAT(NotifyLevel, '","mark_unread":"` + model.CHANNEL_MARK_UNREAD_ALL + `"}'))`)
+ if err != nil {
+ l4g.Error("Unable to set default values for ChannelMembers.NotifyProps")
+ l4g.Error(err.Error())
+ }
+
+ // assume channels with all notifications enabled are just using the default settings
+ _, err = s.GetMaster().Exec(
+ `UPDATE
+ ChannelMembers
+ SET
+ NotifyProps = '{"desktop":"` + model.CHANNEL_NOTIFY_DEFAULT + `","mark_unread":"` + model.CHANNEL_MARK_UNREAD_ALL + `"}'
+ WHERE
+ NotifyLevel = '` + model.CHANNEL_NOTIFY_ALL + `'`)
+ if err != nil {
+ l4g.Error("Unable to set values for ChannelMembers.NotifyProps when members previously had notifyLevel=all")
+ l4g.Error(err.Error())
+ }
+
+ // set quiet mode channels to have no notifications and only mark the channel unread on mentions
+ _, err = s.GetMaster().Exec(
+ `UPDATE
+ ChannelMembers
+ SET
+ NotifyProps = '{"desktop":"` + model.CHANNEL_NOTIFY_NONE + `","mark_unread":"` + model.CHANNEL_MARK_UNREAD_MENTION + `"}'
+ WHERE
+ NotifyLevel = 'quiet'`)
+ if err != nil {
+ l4g.Error("Unable to set values for ChannelMembers.NotifyProps when members previously had notifyLevel=quiet")
+ l4g.Error(err.Error())
+ }
+
+ s.RemoveColumnIfExists("ChannelMembers", "NotifyLevel")
+ }
}
func (s SqlChannelStore) CreateIndexesIfNotExists() {
@@ -386,6 +429,34 @@ func (s SqlChannelStore) SaveMember(member *model.ChannelMember) StoreChannel {
return storeChannel
}
+func (s SqlChannelStore) UpdateMember(member *model.ChannelMember) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ member.PreUpdate()
+
+ if result.Err = member.IsValid(); result.Err != nil {
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ if _, err := s.GetMaster().Update(member); err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.UpdateMember", "We encounted an error updating the channel member",
+ "channel_id="+member.ChannelId+", "+"user_id="+member.UserId+", "+err.Error())
+ } else {
+ result.Data = member
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
func (s SqlChannelStore) GetMembers(channelId string) StoreChannel {
storeChannel := make(StoreChannel)
@@ -649,35 +720,6 @@ func (s SqlChannelStore) IncrementMentionCount(channelId string, userId string)
return storeChannel
}
-func (s SqlChannelStore) UpdateNotifyLevel(channelId, userId, notifyLevel string) StoreChannel {
- storeChannel := make(StoreChannel)
-
- go func() {
- result := StoreResult{}
-
- updateAt := model.GetMillis()
-
- _, err := s.GetMaster().Exec(
- `UPDATE
- ChannelMembers
- SET
- NotifyLevel = :NotifyLevel,
- LastUpdateAt = :LastUpdateAt
- WHERE
- UserId = :UserId
- AND ChannelId = :ChannelId`,
- map[string]interface{}{"ChannelId": channelId, "UserId": userId, "NotifyLevel": notifyLevel, "LastUpdateAt": updateAt})
- if err != nil {
- result.Err = model.NewAppError("SqlChannelStore.UpdateNotifyLevel", "We couldn't update the notify level", "channel_id="+channelId+", user_id="+userId+", "+err.Error())
- }
-
- storeChannel <- result
- close(storeChannel)
- }()
-
- return storeChannel
-}
-
func (s SqlChannelStore) GetForExport(teamId string) StoreChannel {
storeChannel := make(StoreChannel)
diff --git a/store/sql_channel_store_test.go b/store/sql_channel_store_test.go
index dabe39904..b6d05684b 100644
--- a/store/sql_channel_store_test.go
+++ b/store/sql_channel_store_test.go
@@ -135,13 +135,13 @@ func TestChannelStoreDelete(t *testing.T) {
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
- m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1))
m2 := model.ChannelMember{}
m2.ChannelId = o2.Id
m2.UserId = m1.UserId
- m2.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m2.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m2))
if r := <-store.Channel().Delete(o1.Id, model.GetMillis()); r.Err != nil {
@@ -222,13 +222,13 @@ func TestChannelMemberStore(t *testing.T) {
o1 := model.ChannelMember{}
o1.ChannelId = c1.Id
o1.UserId = u1.Id
- o1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ o1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&o1))
o2 := model.ChannelMember{}
o2.ChannelId = c1.Id
o2.UserId = u2.Id
- o2.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ o2.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&o2))
c1t2 := (<-store.Channel().Get(c1.Id)).Data.(*model.Channel)
@@ -291,7 +291,7 @@ func TestChannelStorePermissionsTo(t *testing.T) {
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
- m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1))
count := (<-store.Channel().CheckPermissionsTo(o1.TeamId, o1.Id, m1.UserId)).Data.(int64)
@@ -371,19 +371,19 @@ func TestChannelStoreGetChannels(t *testing.T) {
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
- m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1))
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = model.NewId()
- m2.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m2.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m2))
m3 := model.ChannelMember{}
m3.ChannelId = o2.Id
m3.UserId = model.NewId()
- m3.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m3.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m3))
cresult := <-store.Channel().GetChannels(o1.TeamId, m1.UserId)
@@ -414,19 +414,19 @@ func TestChannelStoreGetMoreChannels(t *testing.T) {
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
- m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1))
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = model.NewId()
- m2.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m2.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m2))
m3 := model.ChannelMember{}
m3.ChannelId = o2.Id
m3.UserId = model.NewId()
- m3.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m3.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m3))
o3 := model.Channel{}
@@ -482,19 +482,19 @@ func TestChannelStoreGetChannelCounts(t *testing.T) {
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
- m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1))
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = model.NewId()
- m2.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m2.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m2))
m3 := model.ChannelMember{}
m3.ChannelId = o2.Id
m3.UserId = model.NewId()
- m3.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m3.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m3))
cresult := <-store.Channel().GetChannelCounts(o1.TeamId, m1.UserId)
@@ -523,7 +523,7 @@ func TestChannelStoreUpdateLastViewedAt(t *testing.T) {
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
- m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1))
err := (<-store.Channel().UpdateLastViewedAt(m1.ChannelId, m1.UserId)).Err
@@ -551,7 +551,7 @@ func TestChannelStoreIncrementMentionCount(t *testing.T) {
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
- m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1))
err := (<-store.Channel().IncrementMentionCount(m1.ChannelId, m1.UserId)).Err
diff --git a/store/sql_post_store_test.go b/store/sql_post_store_test.go
index 257054033..6a6364dc8 100644
--- a/store/sql_post_store_test.go
+++ b/store/sql_post_store_test.go
@@ -484,7 +484,7 @@ func TestPostStoreSearch(t *testing.T) {
m1 := model.ChannelMember{}
m1.ChannelId = c1.Id
m1.UserId = userId
- m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1))
c2 := &model.Channel{}
diff --git a/store/store.go b/store/store.go
index 23580f452..887913bc6 100644
--- a/store/store.go
+++ b/store/store.go
@@ -62,6 +62,7 @@ type ChannelStore interface {
GetForExport(teamId string) StoreChannel
SaveMember(member *model.ChannelMember) StoreChannel
+ UpdateMember(member *model.ChannelMember) StoreChannel
GetMembers(channelId string) StoreChannel
GetMember(channelId string, userId string) StoreChannel
RemoveMember(channelId string, userId string) StoreChannel
@@ -71,7 +72,6 @@ type ChannelStore interface {
CheckPermissionsToByName(teamId string, channelName string, userId string) StoreChannel
UpdateLastViewedAt(channelId string, userId string) StoreChannel
IncrementMentionCount(channelId string, userId string) StoreChannel
- UpdateNotifyLevel(channelId string, userId string, notifyLevel string) StoreChannel
}
type PostStore interface {
diff --git a/web/react/components/admin_console/sql_settings.jsx b/web/react/components/admin_console/sql_settings.jsx
index 430a7453b..0e0ceb9af 100644
--- a/web/react/components/admin_console/sql_settings.jsx
+++ b/web/react/components/admin_console/sql_settings.jsx
@@ -73,6 +73,12 @@ export default class SqlSettings extends React.Component {
handleGenerate(e) {
e.preventDefault();
+
+ var cfm = global.window.confirm('Warning: re-generating this salt may cause some columns in the database to return empty results.');
+ if (cfm === false) {
+ return;
+ }
+
React.findDOMNode(this.refs.AtRestEncryptKey).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
var s = {saveNeeded: true, serverError: this.state.serverError};
this.setState(s);
diff --git a/web/react/components/channel_notifications.jsx b/web/react/components/channel_notifications.jsx
index 9eda68b38..45981b295 100644
--- a/web/react/components/channel_notifications.jsx
+++ b/web/react/components/channel_notifications.jsx
@@ -15,14 +15,24 @@ export default class ChannelNotifications extends React.Component {
this.onListenerChange = this.onListenerChange.bind(this);
this.updateSection = this.updateSection.bind(this);
- this.handleUpdate = this.handleUpdate.bind(this);
- this.handleRadioClick = this.handleRadioClick.bind(this);
- this.handleQuietToggle = this.handleQuietToggle.bind(this);
- this.createDesktopSection = this.createDesktopSection.bind(this);
- this.createQuietSection = this.createQuietSection.bind(this);
- this.state = {notifyLevel: '', title: '', channelId: '', activeSection: ''};
+ this.handleSubmitNotifyLevel = this.handleSubmitNotifyLevel.bind(this);
+ this.handleUpdateNotifyLevel = this.handleUpdateNotifyLevel.bind(this);
+ this.createNotifyLevelSection = this.createNotifyLevelSection.bind(this);
+
+ this.handleSubmitMarkUnreadLevel = this.handleSubmitMarkUnreadLevel.bind(this);
+ this.handleUpdateMarkUnreadLevel = this.handleUpdateMarkUnreadLevel.bind(this);
+ this.createMarkUnreadLevelSection = this.createMarkUnreadLevelSection.bind(this);
+
+ this.state = {
+ notifyLevel: '',
+ markUnreadLevel: '',
+ title: '',
+ channelId: '',
+ activeSection: ''
+ };
}
+
componentDidMount() {
ChannelStore.addChangeListener(this.onListenerChange);
@@ -30,33 +40,34 @@ export default class ChannelNotifications extends React.Component {
var button = e.relatedTarget;
var channelId = button.getAttribute('data-channelid');
- var notifyLevel = ChannelStore.getMember(channelId).notify_level;
- var quietMode = false;
-
- if (notifyLevel === 'quiet') {
- quietMode = true;
- }
+ const member = ChannelStore.getMember(channelId);
+ var notifyLevel = member.notify_props.desktop;
+ var markUnreadLevel = member.notify_props.mark_unread;
- this.setState({notifyLevel: notifyLevel, quietMode: quietMode, title: button.getAttribute('data-title'), channelId: channelId});
+ this.setState({
+ notifyLevel,
+ markUnreadLevel,
+ title: button.getAttribute('data-title'),
+ channelId: channelId
+ });
}.bind(this));
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onListenerChange);
}
+
onListenerChange() {
if (!this.state.channelId) {
return;
}
- var notifyLevel = ChannelStore.getMember(this.state.channelId).notify_level;
- var quietMode = false;
- if (notifyLevel === 'quiet') {
- quietMode = true;
- }
+ const member = ChannelStore.getMember(this.state.channelId);
+ var notifyLevel = member.notify_props.desktop;
+ var markUnreadLevel = member.notify_props.mark_unread;
var newState = this.state;
newState.notifyLevel = notifyLevel;
- newState.quietMode = quietMode;
+ newState.markUnreadLevel = markUnreadLevel;
if (!Utils.areStatesEqual(this.state, newState)) {
this.setState(newState);
@@ -65,53 +76,64 @@ export default class ChannelNotifications extends React.Component {
updateSection(section) {
this.setState({activeSection: section});
}
- handleUpdate() {
+
+ handleSubmitNotifyLevel() {
var channelId = this.state.channelId;
var notifyLevel = this.state.notifyLevel;
- if (this.state.quietMode) {
- notifyLevel = 'quiet';
+
+ if (ChannelStore.getMember(channelId).notify_props.desktop === notifyLevel) {
+ this.updateSection('');
+ return;
}
var data = {};
data.channel_id = channelId;
data.user_id = UserStore.getCurrentId();
- data.notify_level = notifyLevel;
-
- if (!data.notify_level || data.notify_level.length === 0) {
- return;
- }
+ data.desktop = notifyLevel;
- Client.updateNotifyLevel(data,
- function success() {
+ Client.updateNotifyProps(data,
+ () => {
var member = ChannelStore.getMember(channelId);
- member.notify_level = notifyLevel;
+ member.notify_props.desktop = notifyLevel;
ChannelStore.setChannelMember(member);
this.updateSection('');
- }.bind(this),
- function error(err) {
+ },
+ (err) => {
this.setState({serverError: err.message});
- }.bind(this)
+ }
);
}
- handleRadioClick(notifyLevel) {
- this.setState({notifyLevel: notifyLevel, quietMode: false});
- React.findDOMNode(this.refs.modal).focus();
- }
- handleQuietToggle(quietMode) {
- this.setState({notifyLevel: 'none', quietMode: quietMode});
+
+ handleUpdateNotifyLevel(notifyLevel) {
+ this.setState({notifyLevel});
React.findDOMNode(this.refs.modal).focus();
}
- createDesktopSection(serverError) {
+
+ createNotifyLevelSection(serverError) {
var handleUpdateSection;
+ const user = UserStore.getCurrentUser();
+ const globalNotifyLevel = user.notify_props.desktop;
+
+ let globalNotifyLevelName;
+ if (globalNotifyLevel === 'all') {
+ globalNotifyLevelName = 'For all activity';
+ } else if (globalNotifyLevel === 'mention') {
+ globalNotifyLevelName = 'Only for mentions';
+ } else {
+ globalNotifyLevelName = 'Never';
+ }
+
if (this.state.activeSection === 'desktop') {
- var notifyActive = [false, false, false];
- if (this.state.notifyLevel === 'mention') {
- notifyActive[1] = true;
- } else if (this.state.notifyLevel === 'all') {
+ var notifyActive = [false, false, false, false];
+ if (this.state.notifyLevel === 'default') {
notifyActive[0] = true;
- } else {
+ } else if (this.state.notifyLevel === 'all') {
+ notifyActive[1] = true;
+ } else if (this.state.notifyLevel === 'mention') {
notifyActive[2] = true;
+ } else {
+ notifyActive[3] = true;
}
var inputs = [];
@@ -123,9 +145,9 @@ export default class ChannelNotifications extends React.Component {
<input
type='radio'
checked={notifyActive[0]}
- onChange={this.handleRadioClick.bind(this, 'all')}
+ onChange={this.handleUpdateNotifyLevel.bind(this, 'default')}
>
- For all activity
+ {`Global default (${globalNotifyLevelName})`}
</input>
</label>
<br/>
@@ -135,9 +157,9 @@ export default class ChannelNotifications extends React.Component {
<input
type='radio'
checked={notifyActive[1]}
- onChange={this.handleRadioClick.bind(this, 'mention')}
+ onChange={this.handleUpdateNotifyLevel.bind(this, 'all')}
>
- Only for mentions
+ {'For all activity'}
</input>
</label>
<br/>
@@ -147,9 +169,21 @@ export default class ChannelNotifications extends React.Component {
<input
type='radio'
checked={notifyActive[2]}
- onChange={this.handleRadioClick.bind(this, 'none')}
+ onChange={this.handleUpdateNotifyLevel.bind(this, 'mention')}
+ >
+ {'Only for mentions'}
+ </input>
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ checked={notifyActive[3]}
+ onChange={this.handleUpdateNotifyLevel.bind(this, 'none')}
>
- Never
+ {'Never'}
</input>
</label>
</div>
@@ -162,30 +196,19 @@ export default class ChannelNotifications extends React.Component {
e.preventDefault();
}.bind(this);
- let curChannel = ChannelStore.get(this.state.channelId);
- let extraInfo = (
+ const extraInfo = (
<span>
- These settings will override the global notification settings.
+ {'Selecting an option other than "Default" will override the global notification settings.'}
<br/>
- Desktop notifications are available on Firefox, Safari, and Chrome.
+ {'Desktop notifications are available on Firefox, Safari, and Chrome.'}
</span>
);
- if (curChannel && curChannel.display_name) {
- extraInfo = (
- <span>
- These settings will override the global notification settings for the <b>{curChannel.display_name}</b> channel.
- <br/>
- Desktop notifications are available on Firefox, Safari, and Chrome.
- </span>
- );
- }
-
return (
<SettingItemMax
title='Send desktop notifications'
inputs={inputs}
- submit={this.handleUpdate}
+ submit={this.handleSubmitNotifyLevel}
server_error={serverError}
updateSection={handleUpdateSection}
extraInfo={extraInfo}
@@ -194,7 +217,9 @@ export default class ChannelNotifications extends React.Component {
}
var describe;
- if (this.state.notifyLevel === 'mention') {
+ if (this.state.notifyLevel === 'default') {
+ describe = `Global default (${globalNotifyLevelName})`;
+ } else if (this.state.notifyLevel === 'mention') {
describe = 'Only for mentions';
} else if (this.state.notifyLevel === 'all') {
describe = 'For all activity';
@@ -215,101 +240,123 @@ export default class ChannelNotifications extends React.Component {
/>
);
}
- createQuietSection(serverError) {
- var handleUpdateSection;
- if (this.state.activeSection === 'quiet') {
- var quietActive = [false, false];
- if (this.state.quietMode) {
- quietActive[0] = true;
- } else {
- quietActive[1] = true;
+
+ handleSubmitMarkUnreadLevel() {
+ const channelId = this.state.channelId;
+ const markUnreadLevel = this.state.markUnreadLevel;
+
+ if (ChannelStore.getMember(channelId).notify_props.mark_unread === markUnreadLevel) {
+ this.updateSection('');
+ return;
+ }
+
+ const data = {
+ channel_id: channelId,
+ user_id: UserStore.getCurrentId(),
+ mark_unread: markUnreadLevel
+ };
+
+ Client.updateNotifyProps(data,
+ () => {
+ var member = ChannelStore.getMember(channelId);
+ member.notify_props.mark_unread = markUnreadLevel;
+ ChannelStore.setChannelMember(member);
+ this.updateSection('');
+ },
+ (err) => {
+ this.setState({serverError: err.message});
}
+ );
+ }
- var inputs = [];
+ handleUpdateMarkUnreadLevel(markUnreadLevel) {
+ this.setState({markUnreadLevel});
+ React.findDOMNode(this.refs.modal).focus();
+ }
- inputs.push(
+ createMarkUnreadLevelSection(serverError) {
+ let content;
+
+ if (this.state.activeSection === 'markUnreadLevel') {
+ const inputs = [(
<div>
<div className='radio'>
<label>
<input
type='radio'
- checked={quietActive[0]}
- onChange={this.handleQuietToggle.bind(this, true)}
+ checked={this.state.markUnreadLevel === 'all'}
+ onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'all')}
>
- On
+ {'For all unread messages'}
</input>
</label>
- <br/>
+ <br />
</div>
<div className='radio'>
<label>
<input
type='radio'
- checked={quietActive[1]}
- onChange={this.handleQuietToggle.bind(this, false)}
+ checked={this.state.markUnreadLevel === 'mention'}
+ onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'mention')}
>
- Off
+ {'Only for mentions'}
</input>
</label>
- <br/>
+ <br />
</div>
</div>
- );
-
- inputs.push(
- <div>
- <br/>
- Enabling quiet mode will turn off desktop notifications and only mark the channel as unread if you have been mentioned.
- </div>
- );
+ )];
- handleUpdateSection = function updateSection(e) {
+ const handleUpdateSection = function handleUpdateSection(e) {
this.updateSection('');
this.onListenerChange();
e.preventDefault();
}.bind(this);
- return (
+ const extraInfo = <span>{'The channel name is bolded in the sidebar when there are unread messages. Selecting "Only for mentions" will bold the channel only when you are mentioned.'}</span>;
+
+ content = (
<SettingItemMax
- title='Quiet mode'
+ title='Mark Channel Unread'
inputs={inputs}
- submit={this.handleUpdate}
+ submit={this.handleSubmitMarkUnreadLevel}
server_error={serverError}
updateSection={handleUpdateSection}
+ extraInfo={extraInfo}
/>
);
- }
-
- var describe;
- if (this.state.quietMode) {
- describe = 'On';
} else {
- describe = 'Off';
- }
+ let describe;
- handleUpdateSection = function updateSection(e) {
- this.updateSection('quiet');
- e.preventDefault();
- }.bind(this);
+ if (!this.state.markUnreadLevel || this.state.markUnreadLevel === 'all') {
+ describe = 'For all unread messages';
+ } else {
+ describe = 'Only for mentions';
+ }
- return (
- <SettingItemMin
- title='Quiet mode'
- describe={describe}
- updateSection={handleUpdateSection}
- />
- );
+ const handleUpdateSection = function handleUpdateSection(e) {
+ this.updateSection('markUnreadLevel');
+ e.preventDefault();
+ }.bind(this);
+
+ content = (
+ <SettingItemMin
+ title='Mark Channel Unread'
+ describe={describe}
+ updateSection={handleUpdateSection}
+ />
+ );
+ }
+
+ return content;
}
+
render() {
var serverError = null;
if (this.state.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
- var desktopSection = this.createDesktopSection(serverError);
-
- var quietSection = this.createQuietSection(serverError);
-
return (
<div
className='modal fade'
@@ -341,9 +388,9 @@ export default class ChannelNotifications extends React.Component {
>
<br/>
<div className='divider-dark first'/>
- {desktopSection}
+ {this.createNotifyLevelSection(serverError)}
<div className='divider-light'/>
- {quietSection}
+ {this.createMarkUnreadLevelSection(serverError)}
<div className='divider-dark'/>
</div>
</div>
diff --git a/web/react/components/notify_counts.jsx b/web/react/components/notify_counts.jsx
index 0b7c41b62..f34b4669f 100644
--- a/web/react/components/notify_counts.jsx
+++ b/web/react/components/notify_counts.jsx
@@ -15,7 +15,7 @@ function getCountsStateFromStores() {
count += channel.total_msg_count - channelMember.msg_count;
} else if (channelMember.mention_count > 0) {
count += channelMember.mention_count;
- } else if (channelMember.notify_level !== 'quiet' && channel.total_msg_count - channelMember.msg_count > 0) {
+ } else if (channelMember.notify_props.mark_unread !== 'mention' && channel.total_msg_count - channelMember.msg_count > 0) {
count += 1;
}
});
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index 82e746dc0..dba75ac5f 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -150,7 +150,7 @@ 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>;
+ 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'>
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index 6033f200f..6d4b56b7b 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -200,13 +200,17 @@ export default class Sidebar extends React.Component {
}
var channel = ChannelStore.get(msg.channel_id);
- var user = UserStore.getCurrentUser();
- if (user.notify_props && ((user.notify_props.desktop === 'mention' && mentions.indexOf(user.id) === -1 && channel.type !== 'D') || user.notify_props.desktop === 'none')) {
- return;
+ const user = UserStore.getCurrentUser();
+ const member = ChannelStore.getMember(msg.channel_id);
+
+ var notifyLevel = member.notify_props.desktop;
+ if (notifyLevel === 'default') {
+ notifyLevel = user.notify_props.desktop;
}
- var member = ChannelStore.getMember(msg.channel_id);
- if ((member.notify_level === 'mention' && mentions.indexOf(user.id) === -1) || member.notify_level === 'none' || member.notify_level === 'quiet') {
+ if (notifyLevel === 'none') {
+ return;
+ } else if (notifyLevel === 'mention' && mentions.indexOf(user.id) === -1 && channel.type !== 'D') {
return;
}
@@ -330,7 +334,7 @@ export default class Sidebar extends React.Component {
var unread = false;
if (channelMember) {
msgCount = channel.total_msg_count - channelMember.msg_count;
- unread = (msgCount > 0 && channelMember.notify_level !== 'quiet') || channelMember.mention_count > 0;
+ unread = (msgCount > 0 && channelMember.notify_props.mark_unread !== 'mention') || channelMember.mention_count > 0;
}
var titleClass = '';
diff --git a/web/react/components/user_settings/manage_incoming_hooks.jsx b/web/react/components/user_settings/manage_incoming_hooks.jsx
index fa2e2e5e4..899dbcd05 100644
--- a/web/react/components/user_settings/manage_incoming_hooks.jsx
+++ b/web/react/components/user_settings/manage_incoming_hooks.jsx
@@ -148,6 +148,8 @@ export default class ManageIncomingHooks extends React.Component {
return (
<div key='addIncomingHook'>
+ {'Create webhook URLs for channels and private groups. These URLs can be used by outside applications to create posts in any channels or private groups you have access to. The specified channel will be used as the default.'}
+ <br/>
<label className='control-label'>{'Add a new incoming webhook'}</label>
<div className='padding-top'>
<select
diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx
index e9ca0f4a5..c4a137ed8 100644
--- a/web/react/components/user_settings/user_settings_appearance.jsx
+++ b/web/react/components/user_settings/user_settings_appearance.jsx
@@ -214,15 +214,14 @@ export default class UserSettingsAppearance extends React.Component {
<div className='divider-dark first'/>
{themeUI}
<div className='divider-dark'/>
- <br/>
- <a
- href='#'
- className='theme'
- onClick={this.handleImportModal}
- >
- {'Import from Slack'}
- </a>
</div>
+ <br/>
+ <a
+ className='theme'
+ onClick={this.handleImportModal}
+ >
+ {'Import theme colors from Slack'}
+ </a>
</div>
);
}
diff --git a/web/react/components/user_settings/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx
index 42c65ef5d..e83f18aab 100644
--- a/web/react/components/user_settings/user_settings_notifications.jsx
+++ b/web/react/components/user_settings/user_settings_notifications.jsx
@@ -17,7 +17,7 @@ function getNotificationsStateFromStores() {
if (user.notify_props && user.notify_props.desktop_sound) {
sound = user.notify_props.desktop_sound;
}
- var desktop = 'all';
+ var desktop = 'default';
if (user.notify_props && user.notify_props.desktop) {
desktop = user.notify_props.desktop;
}
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index ab2965000..7db3ef30d 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -152,21 +152,23 @@ export function getChannel(id) {
}
export function updateLastViewedAt() {
- if (isCallInProgress('updateLastViewed')) {
+ const channelId = ChannelStore.getCurrentId();
+
+ if (channelId === null) {
return;
}
- if (ChannelStore.getCurrentId() == null) {
+ if (isCallInProgress(`updateLastViewed${channelId}`)) {
return;
}
- callTracker.updateLastViewed = utils.getTimestamp();
+ callTracker[`updateLastViewed${channelId}`] = utils.getTimestamp();
client.updateLastViewedAt(
- ChannelStore.getCurrentId(),
- function updateLastViewedAtSuccess() {
+ channelId,
+ () => {
callTracker.updateLastViewed = 0;
},
- function updateLastViewdAtFailure(err) {
+ (err) => {
callTracker.updateLastViewed = 0;
dispatchError(err, 'updateLastViewedAt');
}
@@ -634,4 +636,4 @@ export function getMyTeam() {
dispatchError(err, 'getMyTeam');
}
);
-} \ No newline at end of file
+}
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index 715e26197..5cb165b4c 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -332,6 +332,20 @@ export function saveConfig(config, success, error) {
});
}
+export function logClientError(msg) {
+ var l = {};
+ l.level = 'ERROR';
+ l.message = msg;
+
+ $.ajax({
+ url: '/api/v1/admin/log_client',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(l)
+ });
+}
+
export function testEmail(config, success, error) {
$.ajax({
url: '/api/v1/admin/test_email',
@@ -568,16 +582,16 @@ export function updateChannelDesc(data, success, error) {
track('api', 'api_channels_desc');
}
-export function updateNotifyLevel(data, success, error) {
+export function updateNotifyProps(data, success, error) {
$.ajax({
- url: '/api/v1/channels/update_notify_level',
+ url: '/api/v1/channels/update_notify_props',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
success,
error: function onError(xhr, status, err) {
- var e = handleError('updateNotifyLevel', xhr, status, err);
+ var e = handleError('updateNotifyProps', xhr, status, err);
error(e);
}
});
diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx
index 56bf49c3f..34e42cbae 100644
--- a/web/react/utils/text_formatting.jsx
+++ b/web/react/utils/text_formatting.jsx
@@ -87,8 +87,10 @@ function autolinkUrls(text, tokens) {
const linkText = match.getMatchedText();
let url = linkText;
- if (url.lastIndexOf('http', 0) !== 0) {
- url = `http://${linkText}`;
+ if (match.getType() === 'email') {
+ url = `mailto:${url}`;
+ } else if (url.lastIndexOf('http', 0) !== 0) {
+ url = `http://${url}`;
}
const index = tokens.size;
diff --git a/web/templates/head.html b/web/templates/head.html
index 2b83119d8..faac4975a 100644
--- a/web/templates/head.html
+++ b/web/templates/head.html
@@ -43,13 +43,32 @@
<script src="/static/js/jquery-dragster/jquery.dragster.js"></script>
<style id="antiClickjack">body{display:none !important;}</style>
+
+ <script>
+ window.onerror = function(msg, url, line, column, stack) {
+ var l = {};
+ l.level = 'ERROR';
+ l.message = 'msg: ' + msg + ' row: ' + line + ' col: ' + column + ' stack: ' + stack + ' url: ' + url;
+
+ $.ajax({
+ url: '/api/v1/admin/log_client',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(l)
+ });
+ }
+ </script>
+
<script src="/static/js/bundle.js"></script>
+
<script type="text/javascript">
if (self === top) {
var blocker = document.getElementById("antiClickjack");
blocker.parentNode.removeChild(blocker);
}
</script>
+
<script type="text/javascript">
if (window.config.SegmentDeveloperKey != null && window.config.SegmentDeveloperKey !== "") {
!function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","group","track","ready","alias","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t<analytics.methods.length;t++){var e=analytics.methods[t];analytics[e]=analytics.factory(e)}analytics.load=function(t){var e=document.createElement("script");e.type="text/javascript";e.async=!0;e.src=("https:"===document.location.protocol?"https://":"http://")+"cdn.segment.com/analytics.js/v1/"+t+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(e,n)};analytics.SNIPPET_VERSION="3.0.1";