diff --git a/README.md b/README.md
index c2b0fbf2d8..b12581444d 100644
--- a/README.md
+++ b/README.md
@@ -10,13 +10,21 @@ Mattermost is a team communication service. It brings team messaging and file sh
We built Mattermost to help teams focus on what matters most to them. It works for us, we hope it works for you too.
+Learn More
+==========
+
+Ask the core team anything at: http://forum.mattermost.org
+Share feature requests and upvotes: http://www.mattermost.org/feature-requests/
+File bugs: http://www.mattermost.org/filing-issues/
+Make a pull request: http://www.mattermost.org/contribute-to-mattermost/
+
Installing the Mattermost
=========================
You're installing "Mattermost Preview", a pre-released 0.50 version intended for an early look at what we're building. While SpinPunch runs this version internally, it's not recommended for production deployments since we can't guarantee API stability or backwards compatibility until our 1.0 version release.
-That said, any issues at all, please let us know on the Mattermost forum at: http://bit.ly/1MY1kul
+That said, any issues at all, please let us know on the Mattermost forum at: http://forum.mattermost.org
Local Machine Setup (Docker)
-----------------------------
@@ -33,38 +41,38 @@ Local Machine Setup (Docker)
6. When docker is done fetching the image, open http://dockerhost:8065/ in your browser
### Ubuntu ###
-1. Follow the instructions at https://docs.docker.com/installation/ubuntulinux/ or use the summery below.
+1. Follow the instructions at https://docs.docker.com/installation/ubuntulinux/ or use the summary below.
-`sudo apt-get update`
+ ``` bash
+ sudo apt-get update
+ sudo apt-get install wget
+ wget -qO- https://get.docker.com/ | sh
+ sudo usermod -aG docker
+ sudo service docker start
+ newgrp docker
+ ```
-`sudo apt-get install wget`
-
-`wget -qO- https://get.docker.com/ | sh`
-
-`sudo usermod -aG docker `
-
-`sudo service docker start`
-
-`newgrp docker`
-
-2. Run `docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:helium
+2. Run `docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:helium`
3. When docker is done fetching the image, open http://localhost:8065/ in your browser
### Arch ###
-1. Install docker using the following commands
+1. Install docker using the following commands:
-`pacman -S docker`
+ ``` bash
+ pacman -S docker
+ systemctl enable docker.service
+ systemctl start docker.service
+ gpasswd -a docker
+ newgrp docker
+ ```
-`systemctl enable docker.service`
+2. Start docker container:
-`systemctl start docker.service`
+ ``` bash
+ docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:helium
+ ```
-`gpasswd -a docker`
-
-`newgrp docker`
-
-2. docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:helium
-3. When docker is done fetching the image, open http://localhost:8065/ in your browser
+3. When docker is done fetching the image, open http://localhost:8065/ in your browser.
### Notes ###
If your ISP blocks port 25 then you may install locally but email will not be sent.
@@ -80,6 +88,8 @@ If you wish to remove mattermost-dev use the following commands
1. `docker stop mattermost-dev`
2. `docker rm -v mattermost-dev`
+If you wish to gain access to the container use the following commands
+1. `docker exec -ti mattermost-dev /bin/bash`
AWS Elastic Beanstalk Setup (Docker)
------------------------------------
@@ -89,7 +99,7 @@ AWS Elastic Beanstalk Setup (Docker)
2. Select "Create New Application" from the top right.
3. Name the application and press next
4. Select "Create a web server" environment.
- 5. If asked, select create and AIM role and instance profile and press next.
+ 5. If asked, select create and IAM role and instance profile and press next.
6. For predefined configuration select docker. For environment type select single instance.
7. For application source, select upload your own and upload Dockerrun.aws.json from docker/Dockerrun.aws.json. Everything else may be left at default.
8. Select an environment name, this is how you will refer to your environment. Make sure the URL is available then press next.
@@ -107,7 +117,7 @@ AWS Elastic Beanstalk Setup (Docker)
18. Modify an existing CNAME record set or create a new one with the name * and the value of the domain you copied in step 1.13.
19. Save the record set
-3. Set the enviroment variable "MATTERMOST\_DOMAIN" to the domain you mapped above (example.com not www.example.com)
+3. Set the environment variable "MATTERMOST\_DOMAIN" to the domain you mapped above (example.com not www.example.com)
20. Return the Elastic Beanstalk from the AWS console.
21. Select the environment you created.
22. Select configuration from the sidebar.
@@ -119,6 +129,11 @@ AWS Elastic Beanstalk Setup (Docker)
26. Return to the dashboard on the sidebar and wait for beanstalk update the environment.
27. Try it out by entering the domain you mapped into your browser.
+Contributing
+------------
+
+To contribute to this open source project please review the Mattermost Contribution Guidelines at http://www.mattermost.org/contribute-to-mattermost/.
+
License
-------
@@ -126,3 +141,4 @@ Most Mattermost source files are made available under the terms of the GNU Affer
As an exception, Admin Tools and Configuration Files are are made available under the terms of the Apache License, version 2.0. See LICENSE.txt for more information.
+
diff --git a/api/channel.go b/api/channel.go
index d3f6ca2de9..c0c2d1548a 100644
--- a/api/channel.go
+++ b/api/channel.go
@@ -57,7 +57,7 @@ func createChannel(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if sc, err := CreateChannel(c, channel, r.URL.Path, true); err != nil {
+ if sc, err := CreateChannel(c, channel, true); err != nil {
c.Err = err
return
} else {
@@ -65,7 +65,7 @@ func createChannel(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
-func CreateChannel(c *Context, channel *model.Channel, path string, addMember bool) (*model.Channel, *model.AppError) {
+func CreateChannel(c *Context, channel *model.Channel, addMember bool) (*model.Channel, *model.AppError) {
if result := <-Srv.Store.Channel().Save(channel); result.Err != nil {
return nil, result.Err
} else {
@@ -100,7 +100,7 @@ func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if sc, err := CreateDirectChannel(c, userId, r.URL.Path); err != nil {
+ if sc, err := CreateDirectChannel(c, userId); err != nil {
c.Err = err
return
} else {
@@ -108,7 +108,7 @@ func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
-func CreateDirectChannel(c *Context, otherUserId string, path string) (*model.Channel, *model.AppError) {
+func CreateDirectChannel(c *Context, otherUserId string) (*model.Channel, *model.AppError) {
if len(otherUserId) != 26 {
return nil, model.NewAppError("CreateDirectChannel", "Invalid other user id ", otherUserId)
}
@@ -132,7 +132,7 @@ func CreateDirectChannel(c *Context, otherUserId string, path string) (*model.Ch
return nil, model.NewAppError("CreateDirectChannel", "Invalid other user id ", otherUserId)
}
- if sc, err := CreateChannel(c, channel, path, true); err != nil {
+ if sc, err := CreateChannel(c, channel, true); err != nil {
return nil, err
} else {
cm := &model.ChannelMember{ChannelId: sc.Id, UserId: otherUserId,
@@ -146,6 +146,23 @@ func CreateDirectChannel(c *Context, otherUserId string, path string) (*model.Ch
}
}
+func CreateDefaultChannels(c *Context, teamId string) ([]*model.Channel, *model.AppError) {
+ townSquare := &model.Channel{DisplayName: "Town Square", Name: "town-square", Type: model.CHANNEL_OPEN, TeamId: teamId}
+
+ if _, err := CreateChannel(c, townSquare, false); err != nil {
+ return nil, err
+ }
+
+ offTopic := &model.Channel{DisplayName: "Off-Topic", Name: "off-topic", Type: model.CHANNEL_OPEN, TeamId: teamId}
+
+ if _, err := CreateChannel(c, offTopic, false); err != nil {
+ return nil, err
+ }
+
+ channels := []*model.Channel{townSquare, offTopic}
+ return channels, nil
+}
+
func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) {
channel := model.ChannelFromJson(r.Body)
@@ -303,7 +320,7 @@ func joinChannel(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
channelId := params["id"]
- JoinChannel(c, channelId, r.URL.Path)
+ JoinChannel(c, channelId, "")
if c.Err != nil {
return
@@ -314,7 +331,7 @@ func joinChannel(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJson(result)))
}
-func JoinChannel(c *Context, channelId string, path string) {
+func JoinChannel(c *Context, channelId string, role string) {
sc := Srv.Store.Channel().Get(channelId)
uc := Srv.Store.User().Get(c.Session.UserId)
@@ -340,7 +357,7 @@ func JoinChannel(c *Context, channelId string, path string) {
}
if channel.Type == model.CHANNEL_OPEN {
- cm := &model.ChannelMember{ChannelId: channel.Id, UserId: c.Session.UserId, NotifyLevel: model.CHANNEL_NOTIFY_ALL}
+ cm := &model.ChannelMember{ChannelId: channel.Id, UserId: c.Session.UserId, NotifyLevel: model.CHANNEL_NOTIFY_ALL, Roles: role}
if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil {
c.Err = cmresult.Err
@@ -363,6 +380,32 @@ func JoinChannel(c *Context, channelId string, path string) {
}
}
+func JoinDefaultChannels(c *Context, user *model.User, channelRole string) *model.AppError {
+ // We don't call JoinChannel here since c.Session is not populated on user creation
+
+ var err *model.AppError = nil
+
+ 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}
+ if cmResult := <-Srv.Store.Channel().SaveMember(cm); cmResult.Err != nil {
+ err = cmResult.Err
+ }
+ }
+
+ 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}
+ if cmResult := <-Srv.Store.Channel().SaveMember(cm); cmResult.Err != nil {
+ err = cmResult.Err
+ }
+ }
+
+ return err
+}
+
func leaveChannel(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
diff --git a/api/channel_test.go b/api/channel_test.go
index e8aaf4e3f9..dfae840dce 100644
--- a/api/channel_test.go
+++ b/api/channel_test.go
@@ -35,8 +35,15 @@ func TestCreateChannel(t *testing.T) {
}
rget := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList)
- if rget.Channels[0].Name != channel.Name {
- t.Fatal("full name didn't match")
+ nameMatch := false
+ for _, c := range rget.Channels {
+ if c.Name == channel.Name {
+ nameMatch = true
+ }
+ }
+
+ if !nameMatch {
+ t.Fatal("Did not create channel with correct name")
}
if _, err := Client.CreateChannel(rchannel.Data.(*model.Channel)); err == nil {
@@ -679,6 +686,8 @@ func TestUpdateNotifyLevel(t *testing.T) {
data["user_id"] = user.Id
data["notify_level"] = model.CHANNEL_NOTIFY_MENTION
+ timeBeforeUpdate := model.GetMillis()
+
if _, err := Client.UpdateNotifyLevel(data); err != nil {
t.Fatal(err)
}
@@ -689,6 +698,10 @@ func TestUpdateNotifyLevel(t *testing.T) {
t.Fatal("NotifyLevel did not update properly")
}
+ if rdata.Members[channel1.Id].LastUpdateAt <= timeBeforeUpdate {
+ t.Fatal("LastUpdateAt did not update")
+ }
+
data["user_id"] = "junk"
if _, err := Client.UpdateNotifyLevel(data); err == nil {
t.Fatal("Should have errored - bad user id")
@@ -735,7 +748,7 @@ func TestUpdateNotifyLevel(t *testing.T) {
}
func TestFuzzyChannel(t *testing.T) {
- Setup();
+ Setup()
team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
@@ -747,9 +760,9 @@ func TestFuzzyChannel(t *testing.T) {
Client.LoginByEmail(team.Domain, user.Email, "pwd")
// Strings that should pass as acceptable channel names
- var fuzzyStringsPass = []string {
+ var fuzzyStringsPass = []string{
"*", "?", ".", "}{][)(><", "{}[]()<>",
-
+
"qahwah ( قهوة)",
"שָׁלוֹם עֲלֵיכֶם",
"Ramen チャーシュー chāshū",
diff --git a/api/command.go b/api/command.go
index aedbe07cc1..810a8a07ed 100644
--- a/api/command.go
+++ b/api/command.go
@@ -197,7 +197,7 @@ func joinCommand(c *Context, command *model.Command) bool {
return false
}
- JoinChannel(c, v.Id, "/command")
+ JoinChannel(c, v.Id, "")
if c.Err != nil {
return false
diff --git a/api/command_test.go b/api/command_test.go
index d3b0da455c..5b77346283 100644
--- a/api/command_test.go
+++ b/api/command_test.go
@@ -129,7 +129,7 @@ func TestJoinCommands(t *testing.T) {
c1 := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList)
- if len(c1.Channels) != 3 { // 3 because of town-square and direct
+ if len(c1.Channels) != 4 { // 4 because of town-square, off-topic and direct
t.Fatal("didn't join channel")
}
diff --git a/api/post.go b/api/post.go
index 99cbdcb852..650f47062d 100644
--- a/api/post.go
+++ b/api/post.go
@@ -227,7 +227,7 @@ func fireAndForgetNotifications(post *model.Post, teamId, teamUrl string) {
channel = result.Data.(*model.Channel)
if channel.Type == model.CHANNEL_DIRECT {
bodyText = "You have one new message."
- subjectText = "New Direct Message"
+ subjectText = "New Private Message"
} else {
bodyText = "You have one new mention."
subjectText = "New Mention"
@@ -273,7 +273,7 @@ func fireAndForgetNotifications(post *model.Post, teamId, teamUrl string) {
} else {
- // Find out who is a member of the channel only keep those profiles
+ // Find out who is a member of the channel, only keep those profiles
if eResult := <-echan; eResult.Err != nil {
l4g.Error("Failed to get channel members channel_id=%v err=%v", post.ChannelId, eResult.Err.Message)
return
@@ -306,13 +306,23 @@ func fireAndForgetNotifications(post *model.Post, teamId, teamUrl string) {
}
}
}
+
+ // Add @all to keywords if user has them turned on
+ if profile.NotifyProps["all"] == "true" {
+ keywordMap["@all"] = append(keywordMap["@all"], profile.Id)
+ }
+
+ // Add @channel to keywords if user has them turned on
+ if profile.NotifyProps["channel"] == "true" {
+ keywordMap["@channel"] = append(keywordMap["@channel"], profile.Id)
+ }
}
// Build a map as a list of unique user_ids that are mentioned in this post
splitF := func(c rune) bool {
return model.SplitRunes[c]
}
- splitMessage := strings.FieldsFunc(strings.Replace(post.Message, " ", " ", -1), splitF)
+ splitMessage := strings.FieldsFunc(post.Message, splitF)
for _, word := range splitMessage {
// Non-case-sensitive check for regular keys
diff --git a/api/team.go b/api/team.go
index 775bc29ae2..15e4e2c17b 100644
--- a/api/team.go
+++ b/api/team.go
@@ -146,10 +146,8 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) {
} else {
rteam := result.Data.(*model.Team)
- channel := &model.Channel{DisplayName: "Town Square", Name: "town-square", Type: model.CHANNEL_OPEN, TeamId: rteam.Id}
-
- if _, err := CreateChannel(c, channel, r.URL.Path, false); err != nil {
- c.Err = err
+ if _, err := CreateDefaultChannels(c, rteam.Id); err != nil {
+ c.Err = nil
return
}
@@ -197,10 +195,8 @@ func createTeam(c *Context, w http.ResponseWriter, r *http.Request) {
} else {
rteam := result.Data.(*model.Team)
- channel := &model.Channel{DisplayName: "Town Square", Name: "town-square", Type: model.CHANNEL_OPEN, TeamId: rteam.Id}
-
- if _, err := CreateChannel(c, channel, r.URL.Path, false); err != nil {
- c.Err = err
+ if _, err := CreateDefaultChannels(c, rteam.Id); err != nil {
+ c.Err = nil
return
}
@@ -488,12 +484,21 @@ func InviteMembers(team *model.Team, user *model.User, invites []string) {
} else {
sender = user.FullName
}
+
+ senderRole := ""
+ if strings.Contains(user.Roles, model.ROLE_ADMIN) || strings.Contains(user.Roles, model.ROLE_SYSTEM_ADMIN) {
+ senderRole = "administrator"
+ } else {
+ senderRole = "member"
+ }
+
subjectPage := NewServerTemplatePage("invite_subject", teamUrl)
subjectPage.Props["SenderName"] = sender
subjectPage.Props["TeamName"] = team.Name
bodyPage := NewServerTemplatePage("invite_body", teamUrl)
bodyPage.Props["TeamName"] = team.Name
bodyPage.Props["SenderName"] = sender
+ bodyPage.Props["SenderStatus"] = senderRole
bodyPage.Props["Email"] = invite
diff --git a/api/team_test.go b/api/team_test.go
index 042c0a2e95..bb77d43a06 100644
--- a/api/team_test.go
+++ b/api/team_test.go
@@ -55,6 +55,11 @@ func TestCreateFromSignupTeam(t *testing.T) {
}
}
+ c1 := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList)
+ if len(c1.Channels) != 2 {
+ t.Fatal("default channels not created")
+ }
+
ts.Data = "garbage"
_, err = Client.CreateTeamFromSignup(&ts)
if err == nil {
@@ -71,6 +76,17 @@ func TestCreateTeam(t *testing.T) {
t.Fatal(err)
}
+ user := &model.User{TeamId: rteam.Data.(*model.Team).Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ Srv.Store.User().VerifyEmail(user.Id)
+
+ Client.LoginByEmail(team.Domain, user.Email, "pwd")
+
+ c1 := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList)
+ if len(c1.Channels) != 2 {
+ t.Fatal("default channels not created")
+ }
+
if rteam.Data.(*model.Team).Name != team.Name {
t.Fatal("full name didn't match")
}
diff --git a/api/templates/error.html b/api/templates/error.html
index ab4d91378b..f38bb81a1e 100644
--- a/api/templates/error.html
+++ b/api/templates/error.html
@@ -5,7 +5,7 @@
-
+
diff --git a/api/templates/invite_body.html b/api/templates/invite_body.html
index 06f48759cf..8be2ac0df2 100644
--- a/api/templates/invite_body.html
+++ b/api/templates/invite_body.html
@@ -18,7 +18,7 @@
You've been invited
-
{{.Props.TeamName}} started using {{.SiteName}}. The team administrator {{.Props.SenderName}}, has invited you to join {{.Props.TeamName}}.
+
{{.Props.TeamName}} started using {{.SiteName}}. The team {{.Props.SenderStatus}} {{.Props.SenderName}}, has invited you to join {{.Props.TeamName}}.
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index 3079917ecd..7d5ef4d338 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -102,7 +102,7 @@ module.exports = React.createClass({
images.push(filenames[i]);
} else if (i < Constants.MAX_DISPLAY_FILES) {
postFiles.push(
-
+
diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx
index 37e3faef2f..169efc766c 100644
--- a/web/react/components/post_list.jsx
+++ b/web/react/components/post_list.jsx
@@ -295,7 +295,7 @@ module.exports = React.createClass({
},
render: function() {
var order = [];
- var posts = {};
+ var posts;
var last_viewed = Number.MAX_VALUE;
@@ -324,13 +324,7 @@ module.exports = React.createClass({
if (order.length > 0 && order.length % Constants.POST_CHUNK_SIZE === 0) {
more_messages = Load more messages;
} else if (channel.type === 'D') {
- var userIds = channel.name.split('__');
- var teammate;
- if (userIds.length === 2 && userIds[0] === user_id) {
- teammate = UserStore.getProfile(userIds[1]);
- } else if (userIds.length === 2 && userIds[1] === user_id) {
- teammate = UserStore.getProfile(userIds[0]);
- }
+ var teammate = utils.getDirectTeammate(channel.id)
if (teammate) {
var teammate_name = teammate.full_name.length > 0 ? teammate.full_name : teammate.username;
@@ -342,13 +336,13 @@ module.exports = React.createClass({
-
{"This is the start of your direct message history with " + teammate_name + "." } {"Direct messages and files shared here are not shown to people outside this area."}
+
{"This is the start of your private message history with " + teammate_name + "." } {"Private messages and files shared here are not shown to people outside this area."}
);
} else {
more_messages = (
-
{"This is the start of your direct message history with this " + strings.Team + "mate. Direct messages and files shared here are not shown to people outside this area."}
+
{"This is the start of your private message history with this " + strings.Team + "mate. Private messages and files shared here are not shown to people outside this area."}
);
}
@@ -356,6 +350,7 @@ module.exports = React.createClass({
var ui_name = channel.display_name
var members = ChannelStore.getCurrentExtraInfo().members;
var creator_name = "";
+ var userStyle = { color: UserStore.getCurrentUser().props.theme }
for (var i = 0; i < members.length; i++) {
if (members[i].roles.indexOf('admin') > -1) {
@@ -382,8 +377,18 @@ module.exports = React.createClass({
@@ -403,55 +408,70 @@ module.exports = React.createClass({
}
var postCtls = [];
- var previousPostDay = posts[order[order.length-1]] ? utils.getDateForUnixTicks(posts[order[order.length-1]].create_at): new Date();
- var currentPostDay = new Date();
- for (var i = order.length-1; i >= 0; i--) {
- var post = posts[order[i]];
- var parentPost;
+ if (posts != undefined) {
+ var previousPostDay = posts[order[order.length-1]] ? utils.getDateForUnixTicks(posts[order[order.length-1]].create_at): new Date();
+ var currentPostDay = new Date();
- if (post.parent_id) {
- parentPost = posts[post.parent_id];
- } else {
- parentPost = null;
+ for (var i = order.length-1; i >= 0; i--) {
+ var post = posts[order[i]];
+ var parentPost;
+
+ if (post.parent_id) {
+ parentPost = posts[post.parent_id];
+ } else {
+ parentPost = null;
+ }
+
+ var sameUser = i < order.length-1 && posts[order[i+1]].user_id === post.user_id && post.create_at - posts[order[i+1]].create_at <= 1000*60*5 ? "same--user" : "";
+ var sameRoot = i < order.length-1 && post.root_id != "" && (posts[order[i+1]].id === post.root_id || posts[order[i+1]].root_id === post.root_id) ? true : false;
+
+ // we only hide the profile pic if the previous post is not a comment, the current post is not a comment, and the previous post was made by the same user as the current post
+ var hideProfilePic = i < order.length-1 && posts[order[i+1]].user_id === post.user_id && posts[order[i+1]].root_id === '' && post.root_id === '';
+
+ // check if it's the last comment in a consecutive string of comments on the same post
+ var isLastComment = false;
+ if (utils.isComment(post)) {
+ // it is the last comment if it is last post in the channel or the next post has a different root post
+ isLastComment = (i === 0 || posts[order[i-1]].root_id != post.root_id);
+ }
+
+ var postCtl = ;
+
+ currentPostDay = utils.getDateForUnixTicks(post.create_at);
+ if(currentPostDay.getDate() !== previousPostDay.getDate() || currentPostDay.getMonth() !== previousPostDay.getMonth() || currentPostDay.getFullYear() !== previousPostDay.getFullYear()) {
+ postCtls.push(
+
+ );
+ }
+ postCtls.push(postCtl);
+ previousPostDay = utils.getDateForUnixTicks(post.create_at);
}
-
- var sameUser = i < order.length-1 && posts[order[i+1]].user_id === post.user_id && post.create_at - posts[order[i+1]].create_at <= 1000*60*5 ? "same--user" : "";
- var sameRoot = i < order.length-1 && post.root_id != "" && (posts[order[i+1]].id === post.root_id || posts[order[i+1]].root_id === post.root_id) ? true : false;
-
- // we only hide the profile pic if the previous post is not a comment, the current post is not a comment, and the previous post was made by the same user as the current post
- var hideProfilePic = i < order.length-1 && posts[order[i+1]].user_id === post.user_id && posts[order[i+1]].root_id === '' && post.root_id === '';
-
- // check if it's the last comment in a consecutive string of comments on the same post
- var isLastComment = false;
- if (utils.isComment(post)) {
- // it is the last comment if it is last post in the channel or the next post has a different root post
- isLastComment = (i === 0 || posts[order[i-1]].root_id != post.root_id);
- }
-
- var postCtl = ;
-
- currentPostDay = utils.getDateForUnixTicks(post.create_at);
- if(currentPostDay.getDate() !== previousPostDay.getDate() || currentPostDay.getMonth() !== previousPostDay.getMonth() || currentPostDay.getFullYear() !== previousPostDay.getFullYear()) {
- postCtls.push(
-
: "" }
{ invite_link }
{ team_link }
{ manage_link }
diff --git a/web/react/components/signup_team_complete.jsx b/web/react/components/signup_team_complete.jsx
index 30fe92af5d..587d8cb82c 100644
--- a/web/react/components/signup_team_complete.jsx
+++ b/web/react/components/signup_team_complete.jsx
@@ -164,7 +164,9 @@ TeamUrlPage = React.createClass({
}
var cleaned_name = utils.cleanUpUrlable(name);
- if (cleaned_name != name) {
+
+ var urlRegex = /^[a-z0-9]+([a-z\-0-9]+|(__)?)[a-z0-9]+$/g;
+ if (cleaned_name != name || !urlRegex.test(name)) {
this.setState({name_error: "Must be lowercase alphanumeric characters"});
return;
}
@@ -337,7 +339,7 @@ EmailItem = React.createClass({
return false;
}
else if (email === teamEmail) {
- this.state.email_error = "Please use an a different email than the one used at signup";
+ this.state.email_error = "Please use a different email than the one used at signup";
this.setState(this.state);
return false;
}
diff --git a/web/react/components/team_settings.jsx b/web/react/components/team_settings.jsx
index 0cec30f3ef..166b1f38b6 100644
--- a/web/react/components/team_settings.jsx
+++ b/web/react/components/team_settings.jsx
@@ -78,13 +78,14 @@ var FeatureTab = React.createClass({
-
Warning: Turning on the Valet feature and using it with any third party software increases the risk of a security breach.
+
Valet is a preview feature for enabling a non-user account limited to basic member permissions that can be manipulated by 3rd parties.
IMPORTANT: The preview version of Valet should not be used without a secure connection and a trusted 3rd party, since user credentials are used to connect. OAuth2 will be used in the final release.