Merge branch 'master' of https://github.com/mattermost/platform into plt-375

Conflicts:
	web/react/components/user_settings/user_settings_appearance.jsx
This commit is contained in:
Asaad Mahmood
2015-10-02 22:26:28 +05:00
31 changed files with 575 additions and 284 deletions

View File

@@ -140,11 +140,11 @@ check: install
test: install test: install
@mkdir -p logs @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=600s ./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=60s ./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=600s ./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=600s ./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 ./web || exit 1
benchmark: install benchmark: install
@mkdir -p logs @mkdir -p logs

View File

@@ -23,8 +23,10 @@ func InitAdmin(r *mux.Router) {
sr.Handle("/logs", ApiUserRequired(getLogs)).Methods("GET") sr.Handle("/logs", ApiUserRequired(getLogs)).Methods("GET")
sr.Handle("/config", ApiUserRequired(getConfig)).Methods("GET") sr.Handle("/config", ApiUserRequired(getConfig)).Methods("GET")
sr.Handle("/save_config", ApiUserRequired(saveConfig)).Methods("POST") 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("/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) { 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))) 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) { func getConfig(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.HasSystemAdminPermissions("getConfig") { if !c.HasSystemAdminPermissions("getConfig") {
return return

View File

@@ -23,7 +23,7 @@ func InitChannel(r *mux.Router) {
sr.Handle("/create_direct", ApiUserRequired(createDirectChannel)).Methods("POST") sr.Handle("/create_direct", ApiUserRequired(createDirectChannel)).Methods("POST")
sr.Handle("/update", ApiUserRequired(updateChannel)).Methods("POST") sr.Handle("/update", ApiUserRequired(updateChannel)).Methods("POST")
sr.Handle("/update_desc", ApiUserRequired(updateChannelDesc)).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]+}/", 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]+}/extra_info", ApiUserRequired(getChannelExtraInfo)).Methods("GET")
sr.Handle("/{id:[A-Za-z0-9]+}/join", ApiUserRequired(joinChannel)).Methods("POST") 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 { if addMember {
cm := &model.ChannelMember{ChannelId: sc.Id, UserId: c.Session.UserId, 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 { if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil {
return nil, cmresult.Err 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 { if sc, err := CreateChannel(c, channel, true); err != nil {
return nil, err return nil, err
} else { } else {
cm := &model.ChannelMember{ChannelId: sc.Id, UserId: otherUserId, cm := &model.ChannelMember{ChannelId: sc.Id, UserId: otherUserId, Roles: "", NotifyProps: model.GetDefaultChannelNotifyProps()}
Roles: "", NotifyLevel: model.CHANNEL_NOTIFY_ALL}
if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil { if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil {
return nil, cmresult.Err return nil, cmresult.Err
@@ -372,7 +371,8 @@ func JoinChannel(c *Context, channelId string, role string) {
} }
if channel.Type == model.CHANNEL_OPEN { 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 { if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil {
c.Err = cmresult.Err 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 { if result := <-Srv.Store.Channel().GetByName(user.TeamId, "town-square"); result.Err != nil {
err = result.Err err = result.Err
} else { } 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 { if cmResult := <-Srv.Store.Channel().SaveMember(cm); cmResult.Err != nil {
err = cmResult.Err 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 { if result := <-Srv.Store.Channel().GetByName(user.TeamId, "off-topic"); result.Err != nil {
err = result.Err err = result.Err
} else { } 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 { if cmResult := <-Srv.Store.Channel().SaveMember(cm); cmResult.Err != nil {
err = cmResult.Err err = cmResult.Err
} }
@@ -694,7 +698,7 @@ func addChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
} else { } else {
oUser := oresult.Data.(*model.User) 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 { 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) 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) data := model.MapFromJson(r.Body)
userId := data["user_id"] userId := data["user_id"]
if len(userId) != 26 { if len(userId) != 26 {
c.SetInvalidParam("updateNotifyLevel", "user_id") c.SetInvalidParam("updateMarkUnreadLevel", "user_id")
return return
} }
channelId := data["channel_id"] channelId := data["channel_id"]
if len(channelId) != 26 { if len(channelId) != 26 {
c.SetInvalidParam("updateNotifyLevel", "channel_id") c.SetInvalidParam("updateMarkUnreadLevel", "channel_id")
return
}
notifyLevel := data["notify_level"]
if len(notifyLevel) == 0 || !model.IsChannelNotifyLevelValid(notifyLevel) {
c.SetInvalidParam("updateNotifyLevel", "notify_level")
return return
} }
@@ -814,10 +813,29 @@ func updateNotifyLevel(c *Context, w http.ResponseWriter, r *http.Request) {
return 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 c.Err = result.Err
return return
} }
w.Write([]byte(model.MapToJson(data))) 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)))
}
} }

View File

@@ -255,7 +255,7 @@ func BenchmarkRemoveChannelMember(b *testing.B) {
} }
} }
func BenchmarkUpdateNotifyLevel(b *testing.B) { func BenchmarkUpdateNotifyProps(b *testing.B) {
var ( var (
NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS} NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS}
) )
@@ -273,7 +273,8 @@ func BenchmarkUpdateNotifyLevel(b *testing.B) {
newmap := map[string]string{ newmap := map[string]string{
"channel_id": channels[i].Id, "channel_id": channels[i].Id,
"user_id": user.Id, "user_id": user.Id,
"notify_level": model.CHANNEL_NOTIFY_MENTION, "desktop": model.CHANNEL_NOTIFY_MENTION,
"mark_unread": model.CHANNEL_MARK_UNREAD_MENTION,
} }
data[i] = newmap data[i] = newmap
} }
@@ -282,7 +283,7 @@ func BenchmarkUpdateNotifyLevel(b *testing.B) {
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
for j := range channels { for j := range channels {
Client.Must(Client.UpdateNotifyLevel(data[j])) Client.Must(Client.UpdateNotifyProps(data[j]))
} }
} }
} }

View File

@@ -803,7 +803,7 @@ func TestRemoveChannelMember(t *testing.T) {
} }
func TestUpdateNotifyLevel(t *testing.T) { func TestUpdateNotifyProps(t *testing.T) {
Setup() Setup()
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} 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 := make(map[string]string)
data["channel_id"] = channel1.Id data["channel_id"] = channel1.Id
data["user_id"] = user.Id data["user_id"] = user.Id
data["notify_level"] = model.CHANNEL_NOTIFY_MENTION data["desktop"] = model.CHANNEL_NOTIFY_MENTION
timeBeforeUpdate := model.GetMillis() timeBeforeUpdate := model.GetMillis()
time.Sleep(100 * time.Millisecond) 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) 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("")) rget := Client.Must(Client.GetChannels(""))
rdata := rget.Data.(*model.ChannelList) rdata := rget.Data.(*model.ChannelList)
if len(rdata.Members) == 0 || rdata.Members[channel1.Id].NotifyLevel != data["notify_level"] { if len(rdata.Members) == 0 || rdata.Members[channel1.Id].NotifyProps["desktop"] != data["desktop"] {
t.Fatal("NotifyLevel did not update properly") t.Fatal("NotifyProps[\"desktop\"] did not update properly")
} } else if rdata.Members[channel1.Id].LastUpdateAt <= timeBeforeUpdate {
if rdata.Members[channel1.Id].LastUpdateAt <= timeBeforeUpdate {
t.Fatal("LastUpdateAt did not update") 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" 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") t.Fatal("Should have errored - bad user id")
} }
data["user_id"] = "12345678901234567890123456" 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") t.Fatal("Should have errored - bad user id")
} }
data["user_id"] = user.Id data["user_id"] = user.Id
data["channel_id"] = "junk" 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") t.Fatal("Should have errored - bad channel id")
} }
data["channel_id"] = "12345678901234567890123456" 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") t.Fatal("Should have errored - bad channel id")
} }
data["channel_id"] = channel1.Id data["desktop"] = "junk"
data["notify_level"] = "" data["mark_unread"] = model.CHANNEL_MARK_UNREAD_ALL
if _, err := Client.UpdateNotifyLevel(data); err == nil { if _, err := Client.UpdateNotifyProps(data); err == nil {
t.Fatal("Should have errored - empty notify level") t.Fatal("Should have errored - bad desktop notify level")
} }
data["notify_level"] = "junk" data["desktop"] = model.CHANNEL_NOTIFY_ALL
if _, err := Client.UpdateNotifyLevel(data); err == nil { data["mark_unread"] = "junk"
t.Fatal("Should have errored - bad notify level") 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"} 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["channel_id"] = channel1.Id
data["user_id"] = user2.Id data["user_id"] = user2.Id
data["notify_level"] = model.CHANNEL_NOTIFY_MENTION data["desktop"] = model.CHANNEL_NOTIFY_MENTION
if _, err := Client.UpdateNotifyLevel(data); err == nil { data["mark_unread"] = model.CHANNEL_MARK_UNREAD_MENTION
if _, err := Client.UpdateNotifyProps(data); err == nil {
t.Fatal("Should have errored - user not in channel") t.Fatal("Should have errored - user not in channel")
} }
} }

View File

@@ -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.Hashtags, _ = model.ParseHashtags(post.Message)
post.UserId = c.Session.UserId post.UserId = c.Session.UserId

View File

@@ -9,6 +9,7 @@
### Installation ### Installation
- [Software and Hardware Requirements](install/requirements.md) - [Software and Hardware Requirements](install/requirements.md)
- [Production Installation](install/prod-ubuntu.md)
- [Local Machine Setup ](install/single-container-install.md) - [Local Machine Setup ](install/single-container-install.md)
- [AWS Elastic Beanstalk Setup](install/aws-ebs-setup.md) - [AWS Elastic Beanstalk Setup](install/aws-ebs-setup.md)
- [Developer Machine Setup](install/dev-setup.md) - [Developer Machine Setup](install/dev-setup.md)
@@ -27,3 +28,4 @@
## End User Help ## End User Help
- [Mattermost Markdown Formatting](help/enduser/markdown.md) - [Mattermost Markdown Formatting](help/enduser/markdown.md)
- [Slack Import](https://github.com/mattermost/platform/blob/master/doc/import/slack-import.md)

View File

@@ -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).

View File

@@ -31,7 +31,7 @@
* ``` wget https://github.com/mattermost/platform/releases/download/v1.0.0/mattermost.tar.gz``` * ``` wget https://github.com/mattermost/platform/releases/download/v1.0.0/mattermost.tar.gz```
1. Unzip the Mattermost Server by typing: 1. Unzip the Mattermost Server by typing:
* ``` tar -xvzf mattermost.tar.gz``` * ``` 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. 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`. 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: * Create the directory by typing:
@@ -90,8 +90,7 @@ exec bin/platform
1. Configure Nginx to proxy connections from the internet to the Mattermost Server 1. Configure Nginx to proxy connections from the internet to the Mattermost Server
* Create a configuration for Mattermost * Create a configuration for Mattermost
* ``` sudo touch /etc/nginx/sites-available/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 {
server_name mattermost.example.com; server_name mattermost.example.com;
@@ -177,9 +176,9 @@ exec bin/platform
* Save the Settings * Save the Settings
1. Update File Settings 1. Update File Settings
* Change *Local Directory Location* from `./data/` to `/mattermost/data` * Change *Local Directory Location* from `./data/` to `/mattermost/data`
1. Update Log Settings 1. Update Log Settings.
* Set *Log to The Console* to false * 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 Remote Address* to false
* Set *Vary By HTTP Header* to X-Real-IP * Set *Vary By HTTP Header* to X-Real-IP
1. Feel free to modify other settings. 1. Feel free to modify other settings.

View File

@@ -355,7 +355,7 @@ Usage:
-reset_password Resets the password for a user. It requires the -reset_password Resets the password for a user. It requires the
-team_name, -email and -password flag. -team_name, -email and -password flag.
Example: 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"
` `

View File

@@ -11,10 +11,12 @@ import (
const ( const (
CHANNEL_ROLE_ADMIN = "admin" CHANNEL_ROLE_ADMIN = "admin"
CHANNEL_NOTIFY_DEFAULT = "default"
CHANNEL_NOTIFY_ALL = "all" CHANNEL_NOTIFY_ALL = "all"
CHANNEL_NOTIFY_MENTION = "mention" CHANNEL_NOTIFY_MENTION = "mention"
CHANNEL_NOTIFY_NONE = "none" CHANNEL_NOTIFY_NONE = "none"
CHANNEL_NOTIFY_QUIET = "quiet" CHANNEL_MARK_UNREAD_ALL = "all"
CHANNEL_MARK_UNREAD_MENTION = "mention"
) )
type ChannelMember struct { type ChannelMember struct {
@@ -24,7 +26,7 @@ type ChannelMember struct {
LastViewedAt int64 `json:"last_viewed_at"` LastViewedAt int64 `json:"last_viewed_at"`
MsgCount int64 `json:"msg_count"` MsgCount int64 `json:"msg_count"`
MentionCount int64 `json:"mention_count"` MentionCount int64 `json:"mention_count"`
NotifyLevel string `json:"notify_level"` NotifyProps StringMap `json:"notify_props"`
LastUpdateAt int64 `json:"last_update_at"` LastUpdateAt int64 `json:"last_update_at"`
} }
@@ -64,8 +66,14 @@ func (o *ChannelMember) IsValid() *AppError {
} }
} }
if len(o.NotifyLevel) > 20 || !IsChannelNotifyLevelValid(o.NotifyLevel) { notifyLevel := o.NotifyProps["desktop"]
return NewAppError("ChannelMember.IsValid", "Invalid notify level", "notify_level="+o.NotifyLevel) 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 return nil
@@ -75,6 +83,24 @@ func (o *ChannelMember) PreSave() {
o.LastUpdateAt = GetMillis() o.LastUpdateAt = GetMillis()
} }
func IsChannelNotifyLevelValid(notifyLevel string) bool { func (o *ChannelMember) PreUpdate() {
return notifyLevel == CHANNEL_NOTIFY_ALL || notifyLevel == CHANNEL_NOTIFY_MENTION || notifyLevel == CHANNEL_NOTIFY_NONE || notifyLevel == CHANNEL_NOTIFY_QUIET o.LastUpdateAt = GetMillis()
}
func IsChannelNotifyLevelValid(notifyLevel string) bool {
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,
}
} }

View File

@@ -31,24 +31,34 @@ func TestChannelMemberIsValid(t *testing.T) {
} }
o.Roles = "missing" o.Roles = "missing"
o.NotifyLevel = CHANNEL_NOTIFY_ALL o.NotifyProps = GetDefaultChannelNotifyProps()
o.UserId = NewId() o.UserId = NewId()
if err := o.IsValid(); err == nil { if err := o.IsValid(); err == nil {
t.Fatal("should be invalid") t.Fatal("should be invalid")
} }
o.Roles = CHANNEL_ROLE_ADMIN o.Roles = CHANNEL_ROLE_ADMIN
o.NotifyLevel = "junk" o.NotifyProps["desktop"] = "junk"
if err := o.IsValid(); err == nil { if err := o.IsValid(); err == nil {
t.Fatal("should be invalid") t.Fatal("should be invalid")
} }
o.NotifyLevel = "123456789012345678901" o.NotifyProps["desktop"] = "123456789012345678901"
if err := o.IsValid(); err == nil { if err := o.IsValid(); err == nil {
t.Fatal("should be invalid") 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 { if err := o.IsValid(); err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -450,8 +450,8 @@ func (c *Client) UpdateChannelDesc(data map[string]string) (*Result, *AppError)
} }
} }
func (c *Client) UpdateNotifyLevel(data map[string]string) (*Result, *AppError) { func (c *Client) UpdateNotifyProps(data map[string]string) (*Result, *AppError) {
if r, err := c.DoApiPost("/channels/update_notify_level", MapToJson(data)); err != nil { if r, err := c.DoApiPost("/channels/update_notify_props", MapToJson(data)); err != nil {
return nil, err return nil, err
} else { } else {
return &Result{r.Header.Get(HEADER_REQUEST_ID), return &Result{r.Header.Get(HEADER_REQUEST_ID),

View File

@@ -120,7 +120,10 @@ func (o *Post) PreSave() {
o.OriginalId = "" o.OriginalId = ""
if o.CreateAt == 0 {
o.CreateAt = GetMillis() o.CreateAt = GetMillis()
}
o.UpdateAt = o.CreateAt o.UpdateAt = o.CreateAt
if o.Props == nil { if o.Props == nil {

View File

@@ -83,5 +83,18 @@ func TestPostIsValid(t *testing.T) {
func TestPostPreSave(t *testing.T) { func TestPostPreSave(t *testing.T) {
o := Post{Message: "test"} o := Post{Message: "test"}
o.PreSave() 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() o.Etag()
} }

View File

@@ -4,6 +4,7 @@
package store package store
import ( import (
l4g "code.google.com/p/log4go"
"github.com/mattermost/platform/model" "github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils" "github.com/mattermost/platform/utils"
) )
@@ -30,13 +31,55 @@ func NewSqlChannelStore(sqlStore *SqlStore) ChannelStore {
tablem.ColMap("ChannelId").SetMaxSize(26) tablem.ColMap("ChannelId").SetMaxSize(26)
tablem.ColMap("UserId").SetMaxSize(26) tablem.ColMap("UserId").SetMaxSize(26)
tablem.ColMap("Roles").SetMaxSize(64) tablem.ColMap("Roles").SetMaxSize(64)
tablem.ColMap("NotifyLevel").SetMaxSize(20) tablem.ColMap("NotifyProps").SetMaxSize(2000)
} }
return s return s
} }
func (s SqlChannelStore) UpgradeSchemaIfNeeded() { 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() { func (s SqlChannelStore) CreateIndexesIfNotExists() {
@@ -386,6 +429,34 @@ func (s SqlChannelStore) SaveMember(member *model.ChannelMember) StoreChannel {
return 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 { func (s SqlChannelStore) GetMembers(channelId string) StoreChannel {
storeChannel := make(StoreChannel) storeChannel := make(StoreChannel)
@@ -649,35 +720,6 @@ func (s SqlChannelStore) IncrementMentionCount(channelId string, userId string)
return storeChannel 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 { func (s SqlChannelStore) GetForExport(teamId string) StoreChannel {
storeChannel := make(StoreChannel) storeChannel := make(StoreChannel)

View File

@@ -135,13 +135,13 @@ func TestChannelStoreDelete(t *testing.T) {
m1 := model.ChannelMember{} m1 := model.ChannelMember{}
m1.ChannelId = o1.Id m1.ChannelId = o1.Id
m1.UserId = model.NewId() m1.UserId = model.NewId()
m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1)) Must(store.Channel().SaveMember(&m1))
m2 := model.ChannelMember{} m2 := model.ChannelMember{}
m2.ChannelId = o2.Id m2.ChannelId = o2.Id
m2.UserId = m1.UserId m2.UserId = m1.UserId
m2.NotifyLevel = model.CHANNEL_NOTIFY_ALL m2.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m2)) Must(store.Channel().SaveMember(&m2))
if r := <-store.Channel().Delete(o1.Id, model.GetMillis()); r.Err != nil { 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 := model.ChannelMember{}
o1.ChannelId = c1.Id o1.ChannelId = c1.Id
o1.UserId = u1.Id o1.UserId = u1.Id
o1.NotifyLevel = model.CHANNEL_NOTIFY_ALL o1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&o1)) Must(store.Channel().SaveMember(&o1))
o2 := model.ChannelMember{} o2 := model.ChannelMember{}
o2.ChannelId = c1.Id o2.ChannelId = c1.Id
o2.UserId = u2.Id o2.UserId = u2.Id
o2.NotifyLevel = model.CHANNEL_NOTIFY_ALL o2.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&o2)) Must(store.Channel().SaveMember(&o2))
c1t2 := (<-store.Channel().Get(c1.Id)).Data.(*model.Channel) c1t2 := (<-store.Channel().Get(c1.Id)).Data.(*model.Channel)
@@ -291,7 +291,7 @@ func TestChannelStorePermissionsTo(t *testing.T) {
m1 := model.ChannelMember{} m1 := model.ChannelMember{}
m1.ChannelId = o1.Id m1.ChannelId = o1.Id
m1.UserId = model.NewId() m1.UserId = model.NewId()
m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1)) Must(store.Channel().SaveMember(&m1))
count := (<-store.Channel().CheckPermissionsTo(o1.TeamId, o1.Id, m1.UserId)).Data.(int64) 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 := model.ChannelMember{}
m1.ChannelId = o1.Id m1.ChannelId = o1.Id
m1.UserId = model.NewId() m1.UserId = model.NewId()
m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1)) Must(store.Channel().SaveMember(&m1))
m2 := model.ChannelMember{} m2 := model.ChannelMember{}
m2.ChannelId = o1.Id m2.ChannelId = o1.Id
m2.UserId = model.NewId() m2.UserId = model.NewId()
m2.NotifyLevel = model.CHANNEL_NOTIFY_ALL m2.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m2)) Must(store.Channel().SaveMember(&m2))
m3 := model.ChannelMember{} m3 := model.ChannelMember{}
m3.ChannelId = o2.Id m3.ChannelId = o2.Id
m3.UserId = model.NewId() m3.UserId = model.NewId()
m3.NotifyLevel = model.CHANNEL_NOTIFY_ALL m3.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m3)) Must(store.Channel().SaveMember(&m3))
cresult := <-store.Channel().GetChannels(o1.TeamId, m1.UserId) cresult := <-store.Channel().GetChannels(o1.TeamId, m1.UserId)
@@ -414,19 +414,19 @@ func TestChannelStoreGetMoreChannels(t *testing.T) {
m1 := model.ChannelMember{} m1 := model.ChannelMember{}
m1.ChannelId = o1.Id m1.ChannelId = o1.Id
m1.UserId = model.NewId() m1.UserId = model.NewId()
m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1)) Must(store.Channel().SaveMember(&m1))
m2 := model.ChannelMember{} m2 := model.ChannelMember{}
m2.ChannelId = o1.Id m2.ChannelId = o1.Id
m2.UserId = model.NewId() m2.UserId = model.NewId()
m2.NotifyLevel = model.CHANNEL_NOTIFY_ALL m2.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m2)) Must(store.Channel().SaveMember(&m2))
m3 := model.ChannelMember{} m3 := model.ChannelMember{}
m3.ChannelId = o2.Id m3.ChannelId = o2.Id
m3.UserId = model.NewId() m3.UserId = model.NewId()
m3.NotifyLevel = model.CHANNEL_NOTIFY_ALL m3.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m3)) Must(store.Channel().SaveMember(&m3))
o3 := model.Channel{} o3 := model.Channel{}
@@ -482,19 +482,19 @@ func TestChannelStoreGetChannelCounts(t *testing.T) {
m1 := model.ChannelMember{} m1 := model.ChannelMember{}
m1.ChannelId = o1.Id m1.ChannelId = o1.Id
m1.UserId = model.NewId() m1.UserId = model.NewId()
m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1)) Must(store.Channel().SaveMember(&m1))
m2 := model.ChannelMember{} m2 := model.ChannelMember{}
m2.ChannelId = o1.Id m2.ChannelId = o1.Id
m2.UserId = model.NewId() m2.UserId = model.NewId()
m2.NotifyLevel = model.CHANNEL_NOTIFY_ALL m2.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m2)) Must(store.Channel().SaveMember(&m2))
m3 := model.ChannelMember{} m3 := model.ChannelMember{}
m3.ChannelId = o2.Id m3.ChannelId = o2.Id
m3.UserId = model.NewId() m3.UserId = model.NewId()
m3.NotifyLevel = model.CHANNEL_NOTIFY_ALL m3.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m3)) Must(store.Channel().SaveMember(&m3))
cresult := <-store.Channel().GetChannelCounts(o1.TeamId, m1.UserId) cresult := <-store.Channel().GetChannelCounts(o1.TeamId, m1.UserId)
@@ -523,7 +523,7 @@ func TestChannelStoreUpdateLastViewedAt(t *testing.T) {
m1 := model.ChannelMember{} m1 := model.ChannelMember{}
m1.ChannelId = o1.Id m1.ChannelId = o1.Id
m1.UserId = model.NewId() m1.UserId = model.NewId()
m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1)) Must(store.Channel().SaveMember(&m1))
err := (<-store.Channel().UpdateLastViewedAt(m1.ChannelId, m1.UserId)).Err err := (<-store.Channel().UpdateLastViewedAt(m1.ChannelId, m1.UserId)).Err
@@ -551,7 +551,7 @@ func TestChannelStoreIncrementMentionCount(t *testing.T) {
m1 := model.ChannelMember{} m1 := model.ChannelMember{}
m1.ChannelId = o1.Id m1.ChannelId = o1.Id
m1.UserId = model.NewId() m1.UserId = model.NewId()
m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1)) Must(store.Channel().SaveMember(&m1))
err := (<-store.Channel().IncrementMentionCount(m1.ChannelId, m1.UserId)).Err err := (<-store.Channel().IncrementMentionCount(m1.ChannelId, m1.UserId)).Err

View File

@@ -484,7 +484,7 @@ func TestPostStoreSearch(t *testing.T) {
m1 := model.ChannelMember{} m1 := model.ChannelMember{}
m1.ChannelId = c1.Id m1.ChannelId = c1.Id
m1.UserId = userId m1.UserId = userId
m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1)) Must(store.Channel().SaveMember(&m1))
c2 := &model.Channel{} c2 := &model.Channel{}

View File

@@ -62,6 +62,7 @@ type ChannelStore interface {
GetForExport(teamId string) StoreChannel GetForExport(teamId string) StoreChannel
SaveMember(member *model.ChannelMember) StoreChannel SaveMember(member *model.ChannelMember) StoreChannel
UpdateMember(member *model.ChannelMember) StoreChannel
GetMembers(channelId string) StoreChannel GetMembers(channelId string) StoreChannel
GetMember(channelId string, userId string) StoreChannel GetMember(channelId string, userId string) StoreChannel
RemoveMember(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 CheckPermissionsToByName(teamId string, channelName string, userId string) StoreChannel
UpdateLastViewedAt(channelId string, userId string) StoreChannel UpdateLastViewedAt(channelId string, userId string) StoreChannel
IncrementMentionCount(channelId string, userId string) StoreChannel IncrementMentionCount(channelId string, userId string) StoreChannel
UpdateNotifyLevel(channelId string, userId string, notifyLevel string) StoreChannel
} }
type PostStore interface { type PostStore interface {

View File

@@ -73,6 +73,12 @@ export default class SqlSettings extends React.Component {
handleGenerate(e) { handleGenerate(e) {
e.preventDefault(); 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); React.findDOMNode(this.refs.AtRestEncryptKey).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
var s = {saveNeeded: true, serverError: this.state.serverError}; var s = {saveNeeded: true, serverError: this.state.serverError};
this.setState(s); this.setState(s);

View File

@@ -15,14 +15,24 @@ export default class ChannelNotifications extends React.Component {
this.onListenerChange = this.onListenerChange.bind(this); this.onListenerChange = this.onListenerChange.bind(this);
this.updateSection = this.updateSection.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() { componentDidMount() {
ChannelStore.addChangeListener(this.onListenerChange); ChannelStore.addChangeListener(this.onListenerChange);
@@ -30,33 +40,34 @@ export default class ChannelNotifications extends React.Component {
var button = e.relatedTarget; var button = e.relatedTarget;
var channelId = button.getAttribute('data-channelid'); var channelId = button.getAttribute('data-channelid');
var notifyLevel = ChannelStore.getMember(channelId).notify_level; const member = ChannelStore.getMember(channelId);
var quietMode = false; var notifyLevel = member.notify_props.desktop;
var markUnreadLevel = member.notify_props.mark_unread;
if (notifyLevel === 'quiet') { this.setState({
quietMode = true; notifyLevel,
} markUnreadLevel,
title: button.getAttribute('data-title'),
this.setState({notifyLevel: notifyLevel, quietMode: quietMode, title: button.getAttribute('data-title'), channelId: channelId}); channelId: channelId
});
}.bind(this)); }.bind(this));
} }
componentWillUnmount() { componentWillUnmount() {
ChannelStore.removeChangeListener(this.onListenerChange); ChannelStore.removeChangeListener(this.onListenerChange);
} }
onListenerChange() { onListenerChange() {
if (!this.state.channelId) { if (!this.state.channelId) {
return; return;
} }
var notifyLevel = ChannelStore.getMember(this.state.channelId).notify_level; const member = ChannelStore.getMember(this.state.channelId);
var quietMode = false; var notifyLevel = member.notify_props.desktop;
if (notifyLevel === 'quiet') { var markUnreadLevel = member.notify_props.mark_unread;
quietMode = true;
}
var newState = this.state; var newState = this.state;
newState.notifyLevel = notifyLevel; newState.notifyLevel = notifyLevel;
newState.quietMode = quietMode; newState.markUnreadLevel = markUnreadLevel;
if (!Utils.areStatesEqual(this.state, newState)) { if (!Utils.areStatesEqual(this.state, newState)) {
this.setState(newState); this.setState(newState);
@@ -65,53 +76,64 @@ export default class ChannelNotifications extends React.Component {
updateSection(section) { updateSection(section) {
this.setState({activeSection: section}); this.setState({activeSection: section});
} }
handleUpdate() {
handleSubmitNotifyLevel() {
var channelId = this.state.channelId; var channelId = this.state.channelId;
var notifyLevel = this.state.notifyLevel; var notifyLevel = this.state.notifyLevel;
if (this.state.quietMode) {
notifyLevel = 'quiet'; if (ChannelStore.getMember(channelId).notify_props.desktop === notifyLevel) {
this.updateSection('');
return;
} }
var data = {}; var data = {};
data.channel_id = channelId; data.channel_id = channelId;
data.user_id = UserStore.getCurrentId(); data.user_id = UserStore.getCurrentId();
data.notify_level = notifyLevel; data.desktop = notifyLevel;
if (!data.notify_level || data.notify_level.length === 0) { Client.updateNotifyProps(data,
return; () => {
}
Client.updateNotifyLevel(data,
function success() {
var member = ChannelStore.getMember(channelId); var member = ChannelStore.getMember(channelId);
member.notify_level = notifyLevel; member.notify_props.desktop = notifyLevel;
ChannelStore.setChannelMember(member); ChannelStore.setChannelMember(member);
this.updateSection(''); this.updateSection('');
}.bind(this), },
function error(err) { (err) => {
this.setState({serverError: err.message}); this.setState({serverError: err.message});
}.bind(this) }
); );
} }
handleRadioClick(notifyLevel) {
this.setState({notifyLevel: notifyLevel, quietMode: false}); handleUpdateNotifyLevel(notifyLevel) {
this.setState({notifyLevel});
React.findDOMNode(this.refs.modal).focus(); React.findDOMNode(this.refs.modal).focus();
} }
handleQuietToggle(quietMode) {
this.setState({notifyLevel: 'none', quietMode: quietMode}); createNotifyLevelSection(serverError) {
React.findDOMNode(this.refs.modal).focus();
}
createDesktopSection(serverError) {
var handleUpdateSection; var handleUpdateSection;
if (this.state.activeSection === 'desktop') { const user = UserStore.getCurrentUser();
var notifyActive = [false, false, false]; const globalNotifyLevel = user.notify_props.desktop;
if (this.state.notifyLevel === 'mention') {
notifyActive[1] = true; let globalNotifyLevelName;
} else if (this.state.notifyLevel === 'all') { if (globalNotifyLevel === 'all') {
notifyActive[0] = true; globalNotifyLevelName = 'For all activity';
} else if (globalNotifyLevel === 'mention') {
globalNotifyLevelName = 'Only for mentions';
} else { } else {
globalNotifyLevelName = 'Never';
}
if (this.state.activeSection === 'desktop') {
var notifyActive = [false, false, false, false];
if (this.state.notifyLevel === 'default') {
notifyActive[0] = true;
} else if (this.state.notifyLevel === 'all') {
notifyActive[1] = true;
} else if (this.state.notifyLevel === 'mention') {
notifyActive[2] = true; notifyActive[2] = true;
} else {
notifyActive[3] = true;
} }
var inputs = []; var inputs = [];
@@ -123,9 +145,9 @@ export default class ChannelNotifications extends React.Component {
<input <input
type='radio' type='radio'
checked={notifyActive[0]} checked={notifyActive[0]}
onChange={this.handleRadioClick.bind(this, 'all')} onChange={this.handleUpdateNotifyLevel.bind(this, 'default')}
> >
For all activity {`Global default (${globalNotifyLevelName})`}
</input> </input>
</label> </label>
<br/> <br/>
@@ -135,9 +157,9 @@ export default class ChannelNotifications extends React.Component {
<input <input
type='radio' type='radio'
checked={notifyActive[1]} checked={notifyActive[1]}
onChange={this.handleRadioClick.bind(this, 'mention')} onChange={this.handleUpdateNotifyLevel.bind(this, 'all')}
> >
Only for mentions {'For all activity'}
</input> </input>
</label> </label>
<br/> <br/>
@@ -147,9 +169,21 @@ export default class ChannelNotifications extends React.Component {
<input <input
type='radio' type='radio'
checked={notifyActive[2]} checked={notifyActive[2]}
onChange={this.handleRadioClick.bind(this, 'none')} onChange={this.handleUpdateNotifyLevel.bind(this, 'mention')}
> >
Never {'Only for mentions'}
</input>
</label>
<br/>
</div>
<div className='radio'>
<label>
<input
type='radio'
checked={notifyActive[3]}
onChange={this.handleUpdateNotifyLevel.bind(this, 'none')}
>
{'Never'}
</input> </input>
</label> </label>
</div> </div>
@@ -162,30 +196,19 @@ export default class ChannelNotifications extends React.Component {
e.preventDefault(); e.preventDefault();
}.bind(this); }.bind(this);
let curChannel = ChannelStore.get(this.state.channelId); const extraInfo = (
let extraInfo = (
<span> <span>
These settings will override the global notification settings. {'Selecting an option other than "Default" will override the global notification settings.'}
<br/> <br/>
Desktop notifications are available on Firefox, Safari, and Chrome. {'Desktop notifications are available on Firefox, Safari, and Chrome.'}
</span> </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 ( return (
<SettingItemMax <SettingItemMax
title='Send desktop notifications' title='Send desktop notifications'
inputs={inputs} inputs={inputs}
submit={this.handleUpdate} submit={this.handleSubmitNotifyLevel}
server_error={serverError} server_error={serverError}
updateSection={handleUpdateSection} updateSection={handleUpdateSection}
extraInfo={extraInfo} extraInfo={extraInfo}
@@ -194,7 +217,9 @@ export default class ChannelNotifications extends React.Component {
} }
var describe; 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'; describe = 'Only for mentions';
} else if (this.state.notifyLevel === 'all') { } else if (this.state.notifyLevel === 'all') {
describe = 'For all activity'; describe = 'For all activity';
@@ -215,28 +240,54 @@ export default class ChannelNotifications extends React.Component {
/> />
); );
} }
createQuietSection(serverError) {
var handleUpdateSection; handleSubmitMarkUnreadLevel() {
if (this.state.activeSection === 'quiet') { const channelId = this.state.channelId;
var quietActive = [false, false]; const markUnreadLevel = this.state.markUnreadLevel;
if (this.state.quietMode) {
quietActive[0] = true; if (ChannelStore.getMember(channelId).notify_props.mark_unread === markUnreadLevel) {
} else { this.updateSection('');
quietActive[1] = true; return;
} }
var inputs = []; const data = {
channel_id: channelId,
user_id: UserStore.getCurrentId(),
mark_unread: markUnreadLevel
};
inputs.push( 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});
}
);
}
handleUpdateMarkUnreadLevel(markUnreadLevel) {
this.setState({markUnreadLevel});
React.findDOMNode(this.refs.modal).focus();
}
createMarkUnreadLevelSection(serverError) {
let content;
if (this.state.activeSection === 'markUnreadLevel') {
const inputs = [(
<div> <div>
<div className='radio'> <div className='radio'>
<label> <label>
<input <input
type='radio' type='radio'
checked={quietActive[0]} checked={this.state.markUnreadLevel === 'all'}
onChange={this.handleQuietToggle.bind(this, true)} onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'all')}
> >
On {'For all unread messages'}
</input> </input>
</label> </label>
<br /> <br />
@@ -245,71 +296,67 @@ export default class ChannelNotifications extends React.Component {
<label> <label>
<input <input
type='radio' type='radio'
checked={quietActive[1]} checked={this.state.markUnreadLevel === 'mention'}
onChange={this.handleQuietToggle.bind(this, false)} onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'mention')}
> >
Off {'Only for mentions'}
</input> </input>
</label> </label>
<br /> <br />
</div> </div>
</div> </div>
); )];
inputs.push( const handleUpdateSection = function handleUpdateSection(e) {
<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) {
this.updateSection(''); this.updateSection('');
this.onListenerChange(); this.onListenerChange();
e.preventDefault(); e.preventDefault();
}.bind(this); }.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 <SettingItemMax
title='Quiet mode' title='Mark Channel Unread'
inputs={inputs} inputs={inputs}
submit={this.handleUpdate} submit={this.handleSubmitMarkUnreadLevel}
server_error={serverError} server_error={serverError}
updateSection={handleUpdateSection} updateSection={handleUpdateSection}
extraInfo={extraInfo}
/> />
); );
}
var describe;
if (this.state.quietMode) {
describe = 'On';
} else { } else {
describe = 'Off'; let describe;
if (!this.state.markUnreadLevel || this.state.markUnreadLevel === 'all') {
describe = 'For all unread messages';
} else {
describe = 'Only for mentions';
} }
handleUpdateSection = function updateSection(e) { const handleUpdateSection = function handleUpdateSection(e) {
this.updateSection('quiet'); this.updateSection('markUnreadLevel');
e.preventDefault(); e.preventDefault();
}.bind(this); }.bind(this);
return ( content = (
<SettingItemMin <SettingItemMin
title='Quiet mode' title='Mark Channel Unread'
describe={describe} describe={describe}
updateSection={handleUpdateSection} updateSection={handleUpdateSection}
/> />
); );
} }
return content;
}
render() { render() {
var serverError = null; var serverError = null;
if (this.state.serverError) { if (this.state.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>; 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 ( return (
<div <div
className='modal fade' className='modal fade'
@@ -341,9 +388,9 @@ export default class ChannelNotifications extends React.Component {
> >
<br/> <br/>
<div className='divider-dark first'/> <div className='divider-dark first'/>
{desktopSection} {this.createNotifyLevelSection(serverError)}
<div className='divider-light'/> <div className='divider-light'/>
{quietSection} {this.createMarkUnreadLevelSection(serverError)}
<div className='divider-dark'/> <div className='divider-dark'/>
</div> </div>
</div> </div>

View File

@@ -15,7 +15,7 @@ function getCountsStateFromStores() {
count += channel.total_msg_count - channelMember.msg_count; count += channel.total_msg_count - channelMember.msg_count;
} else if (channelMember.mention_count > 0) { } else if (channelMember.mention_count > 0) {
count += channelMember.mention_count; 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; count += 1;
} }
}); });

View File

@@ -150,7 +150,7 @@ export default class PostInfo extends React.Component {
var dropdown = this.createDropdown(); 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 ( return (
<ul className='post-header post-info'> <ul className='post-header post-info'>

View File

@@ -200,13 +200,17 @@ export default class Sidebar extends React.Component {
} }
var channel = ChannelStore.get(msg.channel_id); var channel = ChannelStore.get(msg.channel_id);
var user = UserStore.getCurrentUser(); const 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')) { const member = ChannelStore.getMember(msg.channel_id);
return;
var notifyLevel = member.notify_props.desktop;
if (notifyLevel === 'default') {
notifyLevel = user.notify_props.desktop;
} }
var member = ChannelStore.getMember(msg.channel_id); if (notifyLevel === 'none') {
if ((member.notify_level === 'mention' && mentions.indexOf(user.id) === -1) || member.notify_level === 'none' || member.notify_level === 'quiet') { return;
} else if (notifyLevel === 'mention' && mentions.indexOf(user.id) === -1 && channel.type !== 'D') {
return; return;
} }
@@ -330,7 +334,7 @@ export default class Sidebar extends React.Component {
var unread = false; var unread = false;
if (channelMember) { if (channelMember) {
msgCount = channel.total_msg_count - channelMember.msg_count; 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 = ''; var titleClass = '';

View File

@@ -148,6 +148,8 @@ export default class ManageIncomingHooks extends React.Component {
return ( return (
<div key='addIncomingHook'> <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> <label className='control-label'>{'Add a new incoming webhook'}</label>
<div className='padding-top'> <div className='padding-top'>
<select <select

View File

@@ -214,16 +214,15 @@ export default class UserSettingsAppearance extends React.Component {
<div className='divider-dark first'/> <div className='divider-dark first'/>
{themeUI} {themeUI}
<div className='divider-dark'/> <div className='divider-dark'/>
</div>
<br/> <br/>
<a <a
href='#'
className='theme' className='theme'
onClick={this.handleImportModal} onClick={this.handleImportModal}
> >
{'Import from Slack'} {'Import theme colors from Slack'}
</a> </a>
</div> </div>
</div>
); );
} }
} }

View File

@@ -17,7 +17,7 @@ function getNotificationsStateFromStores() {
if (user.notify_props && user.notify_props.desktop_sound) { if (user.notify_props && user.notify_props.desktop_sound) {
sound = 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) { if (user.notify_props && user.notify_props.desktop) {
desktop = user.notify_props.desktop; desktop = user.notify_props.desktop;
} }

View File

@@ -152,21 +152,23 @@ export function getChannel(id) {
} }
export function updateLastViewedAt() { export function updateLastViewedAt() {
if (isCallInProgress('updateLastViewed')) { const channelId = ChannelStore.getCurrentId();
if (channelId === null) {
return; return;
} }
if (ChannelStore.getCurrentId() == null) { if (isCallInProgress(`updateLastViewed${channelId}`)) {
return; return;
} }
callTracker.updateLastViewed = utils.getTimestamp(); callTracker[`updateLastViewed${channelId}`] = utils.getTimestamp();
client.updateLastViewedAt( client.updateLastViewedAt(
ChannelStore.getCurrentId(), channelId,
function updateLastViewedAtSuccess() { () => {
callTracker.updateLastViewed = 0; callTracker.updateLastViewed = 0;
}, },
function updateLastViewdAtFailure(err) { (err) => {
callTracker.updateLastViewed = 0; callTracker.updateLastViewed = 0;
dispatchError(err, 'updateLastViewedAt'); dispatchError(err, 'updateLastViewedAt');
} }

View File

@@ -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) { export function testEmail(config, success, error) {
$.ajax({ $.ajax({
url: '/api/v1/admin/test_email', url: '/api/v1/admin/test_email',
@@ -568,16 +582,16 @@ export function updateChannelDesc(data, success, error) {
track('api', 'api_channels_desc'); track('api', 'api_channels_desc');
} }
export function updateNotifyLevel(data, success, error) { export function updateNotifyProps(data, success, error) {
$.ajax({ $.ajax({
url: '/api/v1/channels/update_notify_level', url: '/api/v1/channels/update_notify_props',
dataType: 'json', dataType: 'json',
contentType: 'application/json', contentType: 'application/json',
type: 'POST', type: 'POST',
data: JSON.stringify(data), data: JSON.stringify(data),
success, success,
error: function onError(xhr, status, err) { error: function onError(xhr, status, err) {
var e = handleError('updateNotifyLevel', xhr, status, err); var e = handleError('updateNotifyProps', xhr, status, err);
error(e); error(e);
} }
}); });

View File

@@ -87,8 +87,10 @@ function autolinkUrls(text, tokens) {
const linkText = match.getMatchedText(); const linkText = match.getMatchedText();
let url = linkText; let url = linkText;
if (url.lastIndexOf('http', 0) !== 0) { if (match.getType() === 'email') {
url = `http://${linkText}`; url = `mailto:${url}`;
} else if (url.lastIndexOf('http', 0) !== 0) {
url = `http://${url}`;
} }
const index = tokens.size; const index = tokens.size;

View File

@@ -43,13 +43,32 @@
<script src="/static/js/jquery-dragster/jquery.dragster.js"></script> <script src="/static/js/jquery-dragster/jquery.dragster.js"></script>
<style id="antiClickjack">body{display:none !important;}</style> <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 src="/static/js/bundle.js"></script>
<script type="text/javascript"> <script type="text/javascript">
if (self === top) { if (self === top) {
var blocker = document.getElementById("antiClickjack"); var blocker = document.getElementById("antiClickjack");
blocker.parentNode.removeChild(blocker); blocker.parentNode.removeChild(blocker);
} }
</script> </script>
<script type="text/javascript"> <script type="text/javascript">
if (window.config.SegmentDeveloperKey != null && window.config.SegmentDeveloperKey !== "") { 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"; !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";