summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Godeps/Godeps.json4
-rw-r--r--README.md10
-rw-r--r--api/command.go23
-rw-r--r--api/file.go1
-rw-r--r--api/post.go20
-rw-r--r--api/post_test.go2
-rw-r--r--api/team.go82
-rw-r--r--api/team_test.go103
-rw-r--r--api/user.go4
-rw-r--r--config/config.json2
-rw-r--r--config/config_docker.json2
-rw-r--r--model/client.go18
-rw-r--r--model/team.go1
-rw-r--r--store/sql_team_store.go6
-rw-r--r--utils/config.go2
-rw-r--r--web/react/components/channel_header.jsx2
-rw-r--r--web/react/components/channel_loader.jsx1
-rw-r--r--web/react/components/create_comment.jsx28
-rw-r--r--web/react/components/create_post.jsx34
-rw-r--r--web/react/components/file_upload.jsx18
-rw-r--r--web/react/components/invite_member_modal.jsx14
-rw-r--r--web/react/components/login.jsx14
-rw-r--r--web/react/components/navbar.jsx2
-rw-r--r--web/react/components/post_list.jsx30
-rw-r--r--web/react/components/settings_sidebar.jsx13
-rw-r--r--web/react/components/sidebar_header.jsx3
-rw-r--r--web/react/components/sidebar_right_menu.jsx2
-rw-r--r--web/react/components/signup_team_complete.jsx22
-rw-r--r--web/react/components/signup_user_complete.jsx10
-rw-r--r--web/react/components/team_settings.jsx161
-rw-r--r--web/react/components/team_settings_modal.jsx (renamed from web/react/components/settings_modal.jsx)14
-rw-r--r--web/react/components/user_settings.jsx5
-rw-r--r--web/react/components/user_settings_modal.jsx68
-rw-r--r--web/react/components/view_image.jsx2
-rw-r--r--web/react/pages/channel.jsx15
-rw-r--r--web/react/stores/team_store.jsx100
-rw-r--r--web/react/utils/async_client.jsx24
-rw-r--r--web/react/utils/client.jsx31
-rw-r--r--web/react/utils/constants.jsx4
-rw-r--r--web/react/utils/utils.jsx21
-rw-r--r--web/sass-files/sass/partials/_post.scss68
-rw-r--r--web/static/config/config.js1
-rw-r--r--web/templates/channel.html3
-rw-r--r--web/web.go1
44 files changed, 855 insertions, 136 deletions
diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json
index 0faccd1ea..093b2a1fc 100644
--- a/Godeps/Godeps.json
+++ b/Godeps/Godeps.json
@@ -136,6 +136,10 @@
"ImportPath": "gopkg.in/redis.v2",
"Comment": "v2.3.2",
"Rev": "e6179049628164864e6e84e973cfb56335748dea"
+ },
+ {
+ "ImportPath": "golang.org/x/image/bmp",
+ "Rev": "eb11b45157c1b71f30b3cec66306f1cd779a689e"
}
]
}
diff --git a/README.md b/README.md
index 4c0631953..d6ead97ee 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ 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://discourse.mattermost.org
Local Machine Setup (Docker)
-----------------------------
@@ -80,6 +80,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)
------------------------------------
@@ -119,6 +121,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 +133,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/command.go b/api/command.go
index 449483bbf..aedbe07cc 100644
--- a/api/command.go
+++ b/api/command.go
@@ -9,6 +9,8 @@ import (
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
"net/http"
+ "reflect"
+ "runtime"
"strconv"
"strings"
)
@@ -19,16 +21,13 @@ var commands = []commandHandler{
logoutCommand,
joinCommand,
loadTestCommand,
+ echoCommand,
}
func InitCommand(r *mux.Router) {
l4g.Debug("Initializing command api routes")
r.Handle("/command", ApiUserRequired(command)).Methods("POST")
- if utils.Cfg.TeamSettings.AllowValet {
- commands = append(commands, echoCommand)
- }
-
hub.Start()
}
@@ -59,6 +58,8 @@ func checkCommand(c *Context, command *model.Command) bool {
return false
}
+ tchan := Srv.Store.Team().Get(c.Session.TeamId)
+
if len(command.ChannelId) > 0 {
cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, command.ChannelId, c.Session.UserId)
@@ -67,7 +68,21 @@ func checkCommand(c *Context, command *model.Command) bool {
}
}
+ allowValet := false
+ if tResult := <-tchan; tResult.Err != nil {
+ c.Err = model.NewAppError("checkCommand", "Could not find the team for this session, team_id="+c.Session.TeamId, "")
+ return false
+ } else {
+ allowValet = tResult.Data.(*model.Team).AllowValet
+ }
+
+ ec := runtime.FuncForPC(reflect.ValueOf(echoCommand).Pointer()).Name()
+
for _, v := range commands {
+ if !allowValet && ec == runtime.FuncForPC(reflect.ValueOf(v).Pointer()).Name() {
+ continue
+ }
+
if v(c, command) {
return true
} else if c.Err != nil {
diff --git a/api/file.go b/api/file.go
index 10167c6ff..0e08567d6 100644
--- a/api/file.go
+++ b/api/file.go
@@ -15,6 +15,7 @@ import (
"github.com/nfnt/resize"
"image"
_ "image/gif"
+ _ "golang.org/x/image/bmp"
"image/jpeg"
"io"
"net/http"
diff --git a/api/post.go b/api/post.go
index 3acc95551..99cbdcb85 100644
--- a/api/post.go
+++ b/api/post.go
@@ -58,11 +58,7 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) {
}
func createValetPost(c *Context, w http.ResponseWriter, r *http.Request) {
- if !utils.Cfg.TeamSettings.AllowValet {
- c.Err = model.NewAppError("createValetPost", "The valet feature is currently turned off. Please contact your system administrator for details.", "")
- c.Err.StatusCode = http.StatusNotImplemented
- return
- }
+ tchan := Srv.Store.Team().Get(c.Session.TeamId)
post := model.PostFromJson(r.Body)
if post == nil {
@@ -70,13 +66,25 @@ func createValetPost(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- // Any one with access to the team can post as valet to any open channel
cchan := Srv.Store.Channel().CheckOpenChannelPermissions(c.Session.TeamId, post.ChannelId)
+ // Any one with access to the team can post as valet to any open channel
if !c.HasPermissionsToChannel(cchan, "createValetPost") {
return
}
+ // Make sure this team has the valet feature enabled
+ if tResult := <-tchan; tResult.Err != nil {
+ c.Err = model.NewAppError("createValetPost", "Could not find the team for this session, team_id="+c.Session.TeamId, "")
+ return
+ } else {
+ if !tResult.Data.(*model.Team).AllowValet {
+ c.Err = model.NewAppError("createValetPost", "The valet feature is currently turned off. Please contact your team administrator for details.", "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
+ }
+
if rp, err := CreateValetPost(c, post); err != nil {
c.Err = err
diff --git a/api/post_test.go b/api/post_test.go
index b322a5017..03f70bff7 100644
--- a/api/post_test.go
+++ b/api/post_test.go
@@ -147,7 +147,7 @@ func TestCreateValetPost(t *testing.T) {
channel2 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel)
- if utils.Cfg.TeamSettings.AllowValet {
+ if utils.Cfg.TeamSettings.AllowValetDefault {
post1 := &model.Post{ChannelId: channel1.Id, Message: "#hashtag a" + model.NewId() + "a"}
rpost1, err := Client.CreateValetPost(post1)
if err != nil {
diff --git a/api/team.go b/api/team.go
index cb60602c6..775bc29ae 100644
--- a/api/team.go
+++ b/api/team.go
@@ -29,6 +29,8 @@ func InitTeam(r *mux.Router) {
sr.Handle("/email_teams", ApiAppHandler(emailTeams)).Methods("POST")
sr.Handle("/invite_members", ApiUserRequired(inviteMembers)).Methods("POST")
sr.Handle("/update_name", ApiUserRequired(updateTeamName)).Methods("POST")
+ sr.Handle("/update_valet_feature", ApiUserRequired(updateValetFeature)).Methods("POST")
+ sr.Handle("/me", ApiUserRequired(getMyTeam)).Methods("GET")
}
func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -136,6 +138,8 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
+ teamSignup.Team.AllowValet = utils.Cfg.TeamSettings.AllowValetDefault
+
if result := <-Srv.Store.Team().Save(&teamSignup.Team); result.Err != nil {
c.Err = result.Err
return
@@ -157,7 +161,7 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if utils.Cfg.TeamSettings.AllowValet {
+ if teamSignup.Team.AllowValet {
CreateValet(c, rteam)
if c.Err != nil {
return
@@ -200,6 +204,13 @@ func createTeam(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
+ if rteam.AllowValet {
+ CreateValet(c, rteam)
+ if c.Err != nil {
+ return
+ }
+ }
+
w.Write([]byte(rteam.ToJson()))
}
}
@@ -542,3 +553,72 @@ func updateTeamName(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJson(props)))
}
+
+func updateValetFeature(c *Context, w http.ResponseWriter, r *http.Request) {
+
+ props := model.MapFromJson(r.Body)
+
+ allowValetStr := props["allow_valet"]
+ if len(allowValetStr) == 0 {
+ c.SetInvalidParam("updateValetFeature", "allow_valet")
+ return
+ }
+
+ allowValet := allowValetStr == "true"
+
+ teamId := props["team_id"]
+ if len(teamId) > 0 && len(teamId) != 26 {
+ c.SetInvalidParam("updateValetFeature", "team_id")
+ return
+ } else if len(teamId) == 0 {
+ teamId = c.Session.TeamId
+ }
+
+ tchan := Srv.Store.Team().Get(teamId)
+
+ if !c.HasPermissionsToTeam(teamId, "updateValetFeature") {
+ return
+ }
+
+ if !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) {
+ c.Err = model.NewAppError("updateValetFeature", "You do not have the appropriate permissions", "userId="+c.Session.UserId)
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+
+ var team *model.Team
+ if tResult := <-tchan; tResult.Err != nil {
+ c.Err = tResult.Err
+ return
+ } else {
+ team = tResult.Data.(*model.Team)
+ }
+
+ team.AllowValet = allowValet
+
+ if result := <-Srv.Store.Team().Update(team); result.Err != nil {
+ c.Err = result.Err
+ return
+ }
+
+ w.Write([]byte(model.MapToJson(props)))
+}
+
+func getMyTeam(c *Context, w http.ResponseWriter, r *http.Request) {
+
+ if len(c.Session.TeamId) == 0 {
+ return
+ }
+
+ if result := <-Srv.Store.Team().Get(c.Session.TeamId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else if HandleEtag(result.Data.(*model.Team).Etag(), w, r) {
+ return
+ } else {
+ w.Header().Set(model.HEADER_ETAG_SERVER, result.Data.(*model.Team).Etag())
+ w.Header().Set("Expires", "-1")
+ w.Write([]byte(result.Data.(*model.Team).ToJson()))
+ return
+ }
+}
diff --git a/api/team_test.go b/api/team_test.go
index 74a184634..042c0a2e9 100644
--- a/api/team_test.go
+++ b/api/team_test.go
@@ -286,3 +286,106 @@ func TestFuzzyTeamCreate(t *testing.T) {
}
}
}
+
+func TestGetMyTeam(t *testing.T) {
+ Setup()
+
+ team := model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ rteam, _ := Client.CreateTeam(&team)
+
+ user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"}
+ ruser, _ := Client.CreateUser(&user, "")
+ Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)
+
+ Client.LoginByEmail(team.Domain, user.Email, user.Password)
+
+ if result, err := Client.GetMyTeam(""); err != nil {
+ t.Fatal("Failed to get user")
+ } else {
+ if result.Data.(*model.Team).Name != team.Name {
+ t.Fatal("team names did not match")
+ }
+ if result.Data.(*model.Team).Domain != team.Domain {
+ t.Fatal("team domains did not match")
+ }
+ if result.Data.(*model.Team).Type != team.Type {
+ t.Fatal("team types did not match")
+ }
+ }
+}
+
+func TestUpdateValetFeature(t *testing.T) {
+ 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)
+
+ user := &model.User{TeamId: team.Id, Email: "test@nowhere.com", FullName: "Corey Hulen", Password: "pwd"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ Srv.Store.User().VerifyEmail(user.Id)
+
+ user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"}
+ user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User)
+ Srv.Store.User().VerifyEmail(user2.Id)
+
+ team2 := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team)
+
+ user3 := &model.User{TeamId: team2.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"}
+ user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User)
+ Srv.Store.User().VerifyEmail(user3.Id)
+
+ Client.LoginByEmail(team.Domain, user2.Email, "pwd")
+
+ data := make(map[string]string)
+ data["allow_valet"] = "true"
+ if _, err := Client.UpdateValetFeature(data); err == nil {
+ t.Fatal("Should have errored, not admin")
+ }
+
+ Client.LoginByEmail(team.Domain, user.Email, "pwd")
+
+ data["allow_valet"] = ""
+ if _, err := Client.UpdateValetFeature(data); err == nil {
+ t.Fatal("Should have errored, empty allow_valet field")
+ }
+
+ data["allow_valet"] = "true"
+ if _, err := Client.UpdateValetFeature(data); err != nil {
+ t.Fatal(err)
+ }
+
+ rteam := Client.Must(Client.GetMyTeam("")).Data.(*model.Team)
+ if rteam.AllowValet != true {
+ t.Fatal("Should have errored - allow valet property not updated")
+ }
+
+ data["team_id"] = "junk"
+ if _, err := Client.UpdateValetFeature(data); err == nil {
+ t.Fatal("Should have errored, junk team id")
+ }
+
+ data["team_id"] = "12345678901234567890123456"
+ if _, err := Client.UpdateValetFeature(data); err == nil {
+ t.Fatal("Should have errored, bad team id")
+ }
+
+ data["team_id"] = team.Id
+ data["allow_valet"] = "false"
+ if _, err := Client.UpdateValetFeature(data); err != nil {
+ t.Fatal(err)
+ }
+
+ rteam = Client.Must(Client.GetMyTeam("")).Data.(*model.Team)
+ if rteam.AllowValet != false {
+ t.Fatal("Should have errored - allow valet property not updated")
+ }
+
+ Client.LoginByEmail(team2.Domain, user3.Email, "pwd")
+
+ data["team_id"] = team.Id
+ data["allow_valet"] = "true"
+ if _, err := Client.UpdateValetFeature(data); err == nil {
+ t.Fatal("Should have errored, not part of team")
+ }
+}
diff --git a/api/user.go b/api/user.go
index 79d4bb32c..f8382cf2f 100644
--- a/api/user.go
+++ b/api/user.go
@@ -145,10 +145,6 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
}
func CreateValet(c *Context, team *model.Team) *model.User {
- if !utils.Cfg.TeamSettings.AllowValet {
- return &model.User{}
- }
-
valet := &model.User{}
valet.TeamId = team.Id
valet.Email = utils.Cfg.EmailSettings.FeedbackEmail
diff --git a/config/config.json b/config/config.json
index c0c236735..e38f1701a 100644
--- a/config/config.json
+++ b/config/config.json
@@ -73,7 +73,7 @@
"TeamSettings": {
"MaxUsersPerTeam": 150,
"AllowPublicLink": true,
- "AllowValet": false,
+ "AllowValetDefault": false,
"TermsLink": "/static/help/configure_links.html",
"PrivacyLink": "/static/help/configure_links.html",
"AboutLink": "/static/help/configure_links.html",
diff --git a/config/config_docker.json b/config/config_docker.json
index 6936f619a..85f0d9c73 100644
--- a/config/config_docker.json
+++ b/config/config_docker.json
@@ -10,7 +10,7 @@
"ServiceSettings": {
"SiteName": "Mattermost",
"Domain": "",
- "Mode" : "prod",
+ "Mode" : "dev",
"AllowTesting" : false,
"UseSSL": false,
"Port": "80",
diff --git a/model/client.go b/model/client.go
index 0448828bb..ab01e7d62 100644
--- a/model/client.go
+++ b/model/client.go
@@ -186,6 +186,15 @@ func (c *Client) UpdateTeamName(data map[string]string) (*Result, *AppError) {
}
}
+func (c *Client) UpdateValetFeature(data map[string]string) (*Result, *AppError) {
+ if r, err := c.DoPost("/teams/update_valet_feature", MapToJson(data)); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
+ }
+}
+
func (c *Client) CreateUser(user *User, hash string) (*Result, *AppError) {
if r, err := c.DoPost("/users/create", user.ToJson()); err != nil {
return nil, err
@@ -647,6 +656,15 @@ func (c *Client) GetStatuses() (*Result, *AppError) {
}
}
+func (c *Client) GetMyTeam(etag string) (*Result, *AppError) {
+ if r, err := c.DoGet("/teams/me", "", etag); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), TeamFromJson(r.Body)}, nil
+ }
+}
+
func (c *Client) MockSession(sessionToken string) {
c.AuthToken = sessionToken
}
diff --git a/model/team.go b/model/team.go
index a510cde78..5c66f3b1f 100644
--- a/model/team.go
+++ b/model/team.go
@@ -24,6 +24,7 @@ type Team struct {
Type string `json:"type"`
CompanyName string `json:"company_name"`
AllowedDomains string `json:"allowed_domains"`
+ AllowValet bool `json:"allow_valet"`
}
type Invites struct {
diff --git a/store/sql_team_store.go b/store/sql_team_store.go
index 6e7fc1c1e..ffb9f8093 100644
--- a/store/sql_team_store.go
+++ b/store/sql_team_store.go
@@ -5,6 +5,7 @@ package store
import (
"github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
"strings"
)
@@ -29,6 +30,11 @@ func NewSqlTeamStore(sqlStore *SqlStore) TeamStore {
}
func (s SqlTeamStore) UpgradeSchemaIfNeeded() {
+ defaultValue := "0"
+ if utils.Cfg.TeamSettings.AllowValetDefault {
+ defaultValue = "1"
+ }
+ s.CreateColumnIfNotExists("Teams", "AllowValet", "AllowedDomains", "tinyint(1)", defaultValue)
}
func (s SqlTeamStore) CreateIndexesIfNotExists() {
diff --git a/utils/config.go b/utils/config.go
index 6a7e4589c..eb2ae3050 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -98,7 +98,7 @@ type PrivacySettings struct {
type TeamSettings struct {
MaxUsersPerTeam int
AllowPublicLink bool
- AllowValet bool
+ AllowValetDefault bool
TermsLink string
PrivacyLink string
AboutLink string
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx
index 006c168ba..ade58a10a 100644
--- a/web/react/components/channel_header.jsx
+++ b/web/react/components/channel_header.jsx
@@ -201,7 +201,7 @@ module.exports = React.createClass({
</a>
<ul className="dropdown-menu" role="menu" aria-labelledby="channel_header_dropdown">
<li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_info" data-channelid={this.state.channel.id} href="#">View Info</a></li>
- <li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_invite" href="#">Invite Members</a></li>
+ <li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_invite" href="#">Add Members</a></li>
{ isAdmin ?
<li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_members" href="#">Manage Members</a></li>
: ""
diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx
index 5252f275c..537a41d03 100644
--- a/web/react/components/channel_loader.jsx
+++ b/web/react/components/channel_loader.jsx
@@ -18,6 +18,7 @@ module.exports = React.createClass({
AsyncClient.getChannelExtraInfo(true);
AsyncClient.findTeams();
AsyncClient.getStatuses();
+ AsyncClient.getMyTeam();
/* End of async loads */
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index cb7aa371c..9e3feb25c 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -112,13 +112,28 @@ module.exports = React.createClass({
return { messageText: '', uploadsInProgress: 0, previews: [], submitting: false };
},
setUploads: function(val) {
- var num = this.state.uploadsInProgress + val;
- this.setState({uploadsInProgress: num});
+ var oldInProgress = this.state.uploadsInProgress
+ var newInProgress = oldInProgress + val;
+
+ if (newInProgress + this.state.previews.length > Constants.MAX_UPLOAD_FILES) {
+ newInProgress = Constants.MAX_UPLOAD_FILES - this.state.previews.length;
+ this.setState({limit_error: "Uploads limited to " + Constants.MAX_UPLOAD_FILES + " files maximum. Please use additional comments for more files."});
+ } else {
+ this.setState({limit_error: null});
+ }
+
+ var numToUpload = newInProgress - oldInProgress;
+ if (numToUpload <= 0) return 0;
+
+ this.setState({uploadsInProgress: newInProgress});
+
+ return numToUpload;
},
render: function() {
var server_error = this.state.server_error ? <div className='form-group has-error'><label className='control-label'>{ this.state.server_error }</label></div> : null;
var post_error = this.state.post_error ? <label className='control-label'>{this.state.post_error}</label> : null;
+ var limit_error = this.state.limit_error ? <div className='has-error'><label className='control-label'>{this.state.limit_error}</label></div> : null;
var preview = <div/>;
if (this.state.previews.length > 0 || this.state.uploadsInProgress > 0) {
@@ -129,13 +144,6 @@ module.exports = React.createClass({
uploadsInProgress={this.state.uploadsInProgress} />
);
}
- var limit_previews = ""
- if (this.state.previews.length > 5) {
- limit_previews = <div className='has-error'><label className='control-label'>{ "Note: While all files will be available, only first five will show thumbnails." }</label></div>
- }
- if (this.state.previews.length > 20) {
- limit_previews = <div className='has-error'><label className='control-label'>{ "Note: Uploads limited to 20 files maximum. Please use additional posts for more files." }</label></div>
- }
return (
<form onSubmit={this.handleSubmit}>
@@ -159,7 +167,7 @@ module.exports = React.createClass({
<input type="button" className="btn btn-primary comment-btn pull-right" value="Add Comment" onClick={this.handleSubmit} />
{ post_error }
{ server_error }
- { limit_previews }
+ { limit_error }
</div>
</div>
{ preview }
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index 5a0b6f85f..0c23dcfac 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -51,7 +51,7 @@ module.exports = React.createClass({
false,
function(data) {
PostStore.storeDraft(data.channel_id, user_id, null);
- this.setState({ messageText: '', submitting: false, post_error: null, previews: [], server_error: null });
+ this.setState({ messageText: '', submitting: false, post_error: null, previews: [], server_error: null, limit_error: null });
if (data.goto_location.length > 0) {
window.location.href = data.goto_location;
@@ -71,7 +71,7 @@ module.exports = React.createClass({
client.createPost(post, ChannelStore.getCurrent(),
function(data) {
PostStore.storeDraft(data.channel_id, data.user_id, null);
- this.setState({ messageText: '', submitting: false, post_error: null, previews: [], server_error: null });
+ this.setState({ messageText: '', submitting: false, post_error: null, previews: [], server_error: null, limit_error: null });
this.resizePostHolder();
AsyncClient.getPosts(true);
@@ -207,21 +207,36 @@ module.exports = React.createClass({
return { channel_id: ChannelStore.getCurrentId(), messageText: messageText, uploadsInProgress: 0, previews: previews, submitting: false, initialText: messageText };
},
setUploads: function(val) {
- var num = this.state.uploadsInProgress + val;
+ var oldInProgress = this.state.uploadsInProgress
+ var newInProgress = oldInProgress + val;
+
+ if (newInProgress + this.state.previews.length > Constants.MAX_UPLOAD_FILES) {
+ newInProgress = Constants.MAX_UPLOAD_FILES - this.state.previews.length;
+ this.setState({limit_error: "Uploads limited to " + Constants.MAX_UPLOAD_FILES + " files maximum. Please use additional posts for more files."});
+ } else {
+ this.setState({limit_error: null});
+ }
+
+ var numToUpload = newInProgress - oldInProgress;
+ if (numToUpload <= 0) return 0;
+
var draft = PostStore.getCurrentDraft();
if (!draft) {
draft = {}
draft['message'] = '';
draft['previews'] = [];
}
- draft['uploadsInProgress'] = num;
+ draft['uploadsInProgress'] = newInProgress;
PostStore.storeCurrentDraft(draft);
- this.setState({uploadsInProgress: num});
+ this.setState({uploadsInProgress: newInProgress});
+
+ return numToUpload;
},
render: function() {
var server_error = this.state.server_error ? <div className='form-group has-error'><label className='control-label'>{ this.state.server_error }</label></div> : null;
var post_error = this.state.post_error ? <label className='control-label'>{this.state.post_error}</label> : null;
+ var limit_error = this.state.limit_error ? <div className='has-error'><label className='control-label'>{this.state.limit_error}</label></div> : null;
var preview = <div/>;
if (this.state.previews.length > 0 || this.state.uploadsInProgress > 0) {
@@ -232,13 +247,6 @@ module.exports = React.createClass({
uploadsInProgress={this.state.uploadsInProgress} />
);
}
- var limit_previews = ""
- if (this.state.previews.length > 5) {
- limit_previews = <div className='has-error'><label className='control-label'>{ "Note: While all files will be available, only first five will show thumbnails." }</label></div>
- }
- if (this.state.previews.length > 20) {
- limit_previews = <div className='has-error'><label className='control-label'>{ "Note: Uploads limited to 20 files maximum. Please use additional posts for more files." }</label></div>
- }
return (
<form id="create_post" ref="topDiv" role="form" onSubmit={this.handleSubmit}>
@@ -260,7 +268,7 @@ module.exports = React.createClass({
<div className={post_error ? 'post-create-footer has-error' : 'post-create-footer'}>
{ post_error }
{ server_error }
- { limit_previews }
+ { limit_error }
{ preview }
<MsgTyping channelId={this.state.channel_id} parentId=""/>
</div>
diff --git a/web/react/components/file_upload.jsx b/web/react/components/file_upload.jsx
index c03a61c63..f2429f17e 100644
--- a/web/react/components/file_upload.jsx
+++ b/web/react/components/file_upload.jsx
@@ -12,18 +12,18 @@ module.exports = React.createClass({
this.props.onUploadError(null);
- //This looks redundant, but must be done this way due to
- //setState being an asynchronous call
+ // This looks redundant, but must be done this way due to
+ // setState being an asynchronous call
var numFiles = 0;
- for(var i = 0; i < files.length && i <= 20 ; i++) {
+ for(var i = 0; i < files.length && i < Constants.MAX_UPLOAD_FILES; i++) {
if (files[i].size <= Constants.MAX_FILE_SIZE) {
numFiles++;
}
}
- this.props.setUploads(numFiles);
+ var numToUpload = this.props.setUploads(numFiles);
- for (var i = 0; i < files.length && i <= 20; i++) {
+ for (var i = 0; i < files.length && i < numToUpload; i++) {
if (files[i].size > Constants.MAX_FILE_SIZE) {
this.props.onUploadError("Files must be no more than " + Constants.MAX_FILE_SIZE/1000000 + " MB");
continue;
@@ -70,8 +70,8 @@ module.exports = React.createClass({
self.props.onUploadError(null);
- //This looks redundant, but must be done this way due to
- //setState being an asynchronous call
+ // This looks redundant, but must be done this way due to
+ // setState being an asynchronous call
var items = e.clipboardData.items;
var numItems = 0;
if (items) {
@@ -87,9 +87,9 @@ module.exports = React.createClass({
}
}
- self.props.setUploads(numItems);
+ var numToUpload = self.props.setUploads(numItems);
- for (var i = 0; i < items.length; i++) {
+ for (var i = 0; i < items.length && i < numToUpload; i++) {
if (items[i].type.indexOf("image") !== -1) {
var file = items[i].getAsFile();
diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx
index 5980664de..d1672126d 100644
--- a/web/react/components/invite_member_modal.jsx
+++ b/web/react/components/invite_member_modal.jsx
@@ -57,7 +57,7 @@ module.exports = React.createClass({
if (config.AllowInviteNames) {
invite.first_name = this.refs["first_name"+index].getDOMNode().value.trim();
- if (!invite.first_name ) {
+ if (!invite.first_name && config.RequireInviteNames) {
first_name_errors[index] = "This is a required field";
valid = false;
} else {
@@ -65,7 +65,7 @@ module.exports = React.createClass({
}
invite.last_name = this.refs["last_name"+index].getDOMNode().value.trim();
- if (!invite.last_name ) {
+ if (!invite.last_name && config.RequireInviteNames) {
last_name_errors[index] = "This is a required field";
valid = false;
} else {
@@ -125,10 +125,12 @@ module.exports = React.createClass({
});
},
removeInviteFields: function(index) {
+ var count = this.state.id_count;
var invite_ids = this.state.invite_ids;
var i = invite_ids.indexOf(index);
- if (index > -1) invite_ids.splice(i, 1);
- this.setState({ invite_ids: invite_ids });
+ if (i > -1) invite_ids.splice(i, 1);
+ if (!invite_ids.length) invite_ids.push(++count);
+ this.setState({ invite_ids: invite_ids, id_count: count });
},
getInitialState: function() {
return {
@@ -154,11 +156,9 @@ module.exports = React.createClass({
invite_sections[index] = (
<div key={"key" + index}>
- { i ?
<div>
- <button type="button" className="btn remove__member" onClick={function(){self.removeInviteFields(index);}}>×</button>
+ <button type="button" className="btn remove__member" onClick={this.removeInviteFields.bind(this, index)}>×</button>
</div>
- : ""}
<div className={ email_error ? "form-group invite has-error" : "form-group invite" }>
<input onKeyUp={this.displayNameKeyUp} type="text" ref={"email"+index} className="form-control" placeholder="email@domain.com" maxLength="64" />
{ email_error }
diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx
index 65f1da1f8..85df5f797 100644
--- a/web/react/components/login.jsx
+++ b/web/react/components/login.jsx
@@ -21,6 +21,12 @@ var FindTeamDomain = React.createClass({
return;
}
+ if (!utils.isLocalStorageSupported()) {
+ state.server_error = "This service requires local storage to be enabled. Please enable it or exit private browsing.";
+ this.setState(state);
+ return;
+ }
+
state.server_error = "";
this.setState(state);
@@ -94,7 +100,7 @@ module.exports = React.createClass({
return;
}
- var email = this.refs.email.getDOMNode().value.trim();
+ var email = this.refs.email.getDOMNode().value.trim();
if (!email) {
state.server_error = "An email is required"
this.setState(state);
@@ -108,6 +114,12 @@ module.exports = React.createClass({
return;
}
+ if (!utils.isLocalStorageSupported()) {
+ state.server_error = "This service requires local storage to be enabled. Please enable it or exit private browsing.";
+ this.setState(state);
+ return;
+ }
+
state.server_error = "";
this.setState(state);
diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx
index 3821c2772..35f7d9044 100644
--- a/web/react/components/navbar.jsx
+++ b/web/react/components/navbar.jsx
@@ -301,7 +301,7 @@ module.exports = React.createClass({
<span className="glyphicon glyphicon-chevron-down header-dropdown__icon"></span>
</a>
<ul className="dropdown-menu" role="menu" aria-labelledby="channel_header_dropdown">
- <li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_invite" href="#">Invite Members</a></li>
+ <li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_invite" href="#">Add Members</a></li>
{ isAdmin ?
<li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_members" href="#">Manage Members</a></li>
: ""
diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx
index 65247b705..37e3faef2 100644
--- a/web/react/components/post_list.jsx
+++ b/web/react/components/post_list.jsx
@@ -125,12 +125,12 @@ module.exports = React.createClass({
$('body').on('mouseenter mouseleave', '.post', function(ev){
if(ev.type === 'mouseenter'){
- $(this).parent('div').prev('.date-seperator').addClass('hovered--after');
- $(this).parent('div').next('.date-seperator').addClass('hovered--before');
+ $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after');
+ $(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--before');
}
else {
- $(this).parent('div').prev('.date-seperator').removeClass('hovered--after');
- $(this).parent('div').next('.date-seperator').removeClass('hovered--before');
+ $(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--after');
+ $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--before');
}
});
@@ -389,7 +389,7 @@ module.exports = React.createClass({
<div className="channel-intro">
<h4 className="channel-intro-title">Welcome</h4>
<p>
- { creator_name != "" ? "This is the start of the " + ui_name + " " + ui_type + ", created by " + creator_name + " on " + utils.displayDate(channel.create_at) + "."
+ { creator_name != "" ? "This is the start of the " + ui_name + " " + ui_type + ", created by " + creator_name + " on " + utils.displayDate(channel.create_at) + "."
: "This is the start of the " + ui_name + " " + ui_type + ", created on "+ utils.displayDate(channel.create_at) + "." }
{ channel.type === 'P' ? " Only invited members can see this private group." : " Any member can join and read this channel." }
<br/>
@@ -434,9 +434,9 @@ module.exports = React.createClass({
currentPostDay = utils.getDateForUnixTicks(post.create_at);
if(currentPostDay.getDate() !== previousPostDay.getDate() || currentPostDay.getMonth() !== previousPostDay.getMonth() || currentPostDay.getFullYear() !== previousPostDay.getFullYear()) {
postCtls.push(
- <div className="date-seperator">
- <hr className="date-seperator__hr" />
- <div className="date-seperator__text">{currentPostDay.toDateString()}</div>
+ <div className="date-separator">
+ <hr className="separator__hr" />
+ <div className="separator__text">{currentPostDay.toDateString()}</div>
</div>
);
}
@@ -444,17 +444,13 @@ module.exports = React.createClass({
if (post.create_at > last_viewed && !rendered_last_viewed) {
rendered_last_viewed = true;
postCtls.push(
- <div>
- <div className="new-seperator">
- <hr id="new_message" className="new-seperator__hr" />
- <div className="new-seperator__text">New Messages</div>
- </div>
- {postCtl}
- </div>
+ <div className="new-separator">
+ <hr id="new_message" className="separator__hr" />
+ <div className="separator__text">New Messages</div>
+ </div>
);
- } else {
- postCtls.push(postCtl);
}
+ postCtls.push(postCtl);
previousPostDay = utils.getDateForUnixTicks(post.create_at);
}
diff --git a/web/react/components/settings_sidebar.jsx b/web/react/components/settings_sidebar.jsx
index a1546890f..ae8510cf2 100644
--- a/web/react/components/settings_sidebar.jsx
+++ b/web/react/components/settings_sidebar.jsx
@@ -1,6 +1,8 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
+var utils = require('../utils/utils.jsx');
+
module.exports = React.createClass({
updateTab: function(tab) {
this.props.updateTab(tab);
@@ -11,16 +13,11 @@ module.exports = React.createClass({
return (
<div className="">
<ul className="nav nav-pills nav-stacked">
- <li className={this.props.activeTab == 'general' ? 'active' : ''}><a href="#" onClick={function(){self.updateTab("general");}}><i className="glyphicon glyphicon-cog"></i>General</a></li>
- <li className={this.props.activeTab == 'security' ? 'active' : ''}><a href="#" onClick={function(){self.updateTab("security");}}><i className="glyphicon glyphicon-lock"></i>Security</a></li>
- <li className={this.props.activeTab == 'notifications' ? 'active' : ''}><a href="#" onClick={function(){self.updateTab("notifications");}}><i className="glyphicon glyphicon-exclamation-sign"></i>Notifications</a></li>
- <li className={this.props.activeTab == 'appearance' ? 'active' : ''}><a href="#" onClick={function(){self.updateTab("appearance");}}><i className="glyphicon glyphicon-wrench"></i>Appearance</a></li>
+ {this.props.tabs.map(function(tab) {
+ return <li className={self.props.activeTab == tab.name ? 'active' : ''}><a href="#" onClick={function(){self.updateTab(tab.name);}}><i className={tab.icon}></i>{tab.ui_name}</a></li>
+ })}
</ul>
</div>
);
- /* Temporarily removing sessions and activity logs
- <li className={this.props.activeTab == 'sessions' ? 'active' : ''}><a href="#" onClick={function(){self.updateTab("sessions");}}><i className="glyphicon glyphicon-globe"></i>Sessions</a></li>
- <li className={this.props.activeTab == 'activity_log' ? 'active' : ''}><a href="#" onClick={function(){self.updateTab("activity_log");}}><i className="glyphicon glyphicon-time"></i>Activity Log</a></li>
- */
}
});
diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx
index 5a872b7a0..0b59d2036 100644
--- a/web/react/components/sidebar_header.jsx
+++ b/web/react/components/sidebar_header.jsx
@@ -94,7 +94,8 @@ var NavbarDropdown = React.createClass({
<i className="dropdown__icon"></i>
</a>
<ul className="dropdown-menu" role="menu">
- <li><a href="#" data-toggle="modal" data-target="#settings_modal">Account Settings</a></li>
+ <li><a href="#" data-toggle="modal" data-target="#user_settings1">Account Settings</a></li>
+ { isAdmin ? <li><a href="#" data-toggle="modal" data-target="#team_settings">Team Settings</a></li> : "" }
{ invite_link }
{ team_link }
{ manage_link }
diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx
index d0c139d1a..c523ce554 100644
--- a/web/react/components/sidebar_right_menu.jsx
+++ b/web/react/components/sidebar_right_menu.jsx
@@ -59,7 +59,7 @@ module.exports = React.createClass({
<div className="nav-pills__container">
<ul className="nav nav-pills nav-stacked">
- <li><a href="#" data-toggle="modal" data-target="#settings_modal"><i className="glyphicon glyphicon-cog"></i>Account Settings</a></li>
+ <li><a href="#" data-toggle="modal" data-target="#user_settings1"><i className="glyphicon glyphicon-cog"></i>Account Settings</a></li>
{ 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 b038679e6..30fe92af5 100644
--- a/web/react/components/signup_team_complete.jsx
+++ b/web/react/components/signup_team_complete.jsx
@@ -9,6 +9,10 @@ var constants = require('../utils/constants.jsx')
WelcomePage = React.createClass({
submitNext: function (e) {
+ if (!utils.isLocalStorageSupported()) {
+ this.setState({ storage_error: "This service requires local storage to be enabled. Please enable it or exit private browsing."} );
+ return;
+ }
e.preventDefault();
this.props.state.wizard = "team_name";
this.props.updateParent(this.props.state);
@@ -26,6 +30,12 @@ WelcomePage = React.createClass({
if (!email || !utils.isEmail(email)) {
state.email_error = "Please enter a valid email address";
this.setState(state);
+ return;
+ }
+ else if (!utils.isLocalStorageSupported()) {
+ state.email_error = "This service requires local storage to be enabled. Please enable it or exit private browsing.";
+ this.setState(state);
+ return;
}
else {
state.email_error = "";
@@ -50,6 +60,7 @@ WelcomePage = React.createClass({
client.track('signup', 'signup_team_01_welcome');
+ var storage_error = this.state.storage_error ? <label className="control-label">{ this.state.storage_error }</label> : null;
var email_error = this.state.email_error ? <label className="control-label">{ this.state.email_error }</label> : null;
var server_error = this.state.server_error ? <div className={ "form-group has-error" }><label className="control-label">{ this.state.server_error }</label></div> : null;
@@ -66,6 +77,7 @@ WelcomePage = React.createClass({
</p>
<div className="form-group">
<button className="btn-primary btn form-group" onClick={this.submitNext}><i className="glyphicon glyphicon-ok"></i>Yes, this address is correct</button>
+ { storage_error }
</div>
<hr />
<p>If this is not correct, you can switch to a different email. We'll send you a new invite right away.</p>
@@ -312,7 +324,7 @@ EmailItem = React.createClass({
getValue: function() {
return this.refs.email.getDOMNode().value.trim()
},
- validate: function() {
+ validate: function(teamEmail) {
var email = this.refs.email.getDOMNode().value.trim().toLowerCase();
if (!email) {
@@ -324,6 +336,11 @@ EmailItem = React.createClass({
this.setState(this.state);
return false;
}
+ else if (email === teamEmail) {
+ this.state.email_error = "Please use an a different email than the one used at signup";
+ this.setState(this.state);
+ return false;
+ }
else {
this.state.email_error = "";
this.setState(this.state);
@@ -363,7 +380,7 @@ SendInivtesPage = React.createClass({
var emails = [];
for (var i = 0; i < this.props.state.invites.length; i++) {
- if (!this.refs['email_' + i].validate()) {
+ if (!this.refs['email_' + i].validate(this.props.state.team.email)) {
valid = false;
} else {
emails.push(this.refs['email_' + i].getValue());
@@ -491,6 +508,7 @@ PasswordPage = React.createClass({
return;
}
+ this.setState({name_error: ""});
$('#finish-button').button('loading');
var teamSignup = JSON.parse(JSON.stringify(this.props.state));
teamSignup.user.password = password;
diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx
index 146419cf5..b9f32f0bc 100644
--- a/web/react/components/signup_user_complete.jsx
+++ b/web/react/components/signup_user_complete.jsx
@@ -13,16 +13,16 @@ module.exports = React.createClass({
this.state.user.username = this.refs.name.getDOMNode().value.trim();
if (!this.state.user.username) {
- this.setState({name_error: "This field is required", email_error: "", password_error: ""});
+ this.setState({name_error: "This field is required", email_error: "", password_error: "", server_error: ""});
return;
}
var username_error = utils.isValidUsername(this.state.user.username)
if (username_error === "Cannot use a reserved word as a username.") {
- this.setState({name_error: "This username is reserved, please choose a new one." });
+ this.setState({name_error: "This username is reserved, please choose a new one.", email_error: "", password_error: "", server_error: ""});
return;
} else if (username_error) {
- this.setState({name_error: "Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'." });
+ this.setState({name_error: "Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'.", email_error: "", password_error: "", server_error: ""});
return;
}
@@ -34,10 +34,12 @@ module.exports = React.createClass({
this.state.user.password = this.refs.password.getDOMNode().value.trim();
if (!this.state.user.password || this.state.user.password .length < 5) {
- this.setState({name_error: "", email_error: "", password_error: "Please enter at least 5 characters"});
+ this.setState({name_error: "", email_error: "", password_error: "Please enter at least 5 characters", server_error: ""});
return;
}
+ this.setState({name_error: "", email_error: "", password_error: "", server_error: ""});
+
this.state.user.allow_marketing = this.refs.email_service.getDOMNode().checked;
client.createUser(this.state.user, this.state.data, this.state.hash,
diff --git a/web/react/components/team_settings.jsx b/web/react/components/team_settings.jsx
new file mode 100644
index 000000000..0cec30f3e
--- /dev/null
+++ b/web/react/components/team_settings.jsx
@@ -0,0 +1,161 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var UserStore = require('../stores/user_store.jsx');
+var TeamStore = require('../stores/team_store.jsx');
+var SettingItemMin = require('./setting_item_min.jsx');
+var SettingItemMax = require('./setting_item_max.jsx');
+var SettingPicture = require('./setting_picture.jsx');
+var utils = require('../utils/utils.jsx');
+
+var client = require('../utils/client.jsx');
+var AsyncClient = require('../utils/async_client.jsx');
+var Constants = require('../utils/constants.jsx');
+
+var FeatureTab = React.createClass({
+ submitValetFeature: function() {
+ data = {};
+ data['allow_valet'] = this.state.allow_valet;
+
+ client.updateValetFeature(data,
+ function(data) {
+ this.props.updateSection("");
+ AsyncClient.getMyTeam();
+ }.bind(this),
+ function(err) {
+ state = this.getInitialState();
+ state.server_error = err;
+ this.setState(state);
+ }.bind(this)
+ );
+ },
+ handleValetRadio: function(val) {
+ this.setState({ allow_valet: val });
+ this.refs.wrapper.getDOMNode().focus();
+ },
+ componentWillReceiveProps: function(newProps) {
+ var team = newProps.team;
+
+ var allow_valet = "false";
+ if (team && team.allow_valet) {
+ allow_valet = "true";
+ }
+
+ this.setState({ allow_valet: allow_valet });
+ },
+ getInitialState: function() {
+ var team = this.props.team;
+
+ var allow_valet = "false";
+ if (team && team.allow_valet) {
+ allow_valet = "true";
+ }
+
+ return { allow_valet: allow_valet };
+ },
+ render: function() {
+ var team = this.props.team;
+
+ var client_error = this.state.client_error ? this.state.client_error : null;
+ var server_error = this.state.server_error ? this.state.server_error : null;
+
+ var valetSection;
+ var self = this;
+
+ if (this.props.activeSection === 'valet') {
+ var valetActive = ["",""];
+ if (this.state.allow_valet === "false") {
+ valetActive[1] = "active";
+ } else {
+ valetActive[0] = "active";
+ }
+
+ var inputs = [];
+
+ inputs.push(
+ <div className="col-sm-12">
+ <div className="btn-group" data-toggle="buttons-radio">
+ <button className={"btn btn-default "+valetActive[0]} onClick={function(){self.handleValetRadio("true")}}>On</button>
+ <button className={"btn btn-default "+valetActive[1]} onClick={function(){self.handleValetRadio("false")}}>Off</button>
+ </div>
+ <div><br/>Warning: Turning on the Valet feature and using it with any third party software increases the risk of a security breach.</div>
+ </div>
+ );
+
+ valetSection = (
+ <SettingItemMax
+ title="Valet"
+ inputs={inputs}
+ submit={this.submitValetFeature}
+ server_error={server_error}
+ client_error={client_error}
+ updateSection={function(e){self.props.updateSection("");e.preventDefault();}}
+ />
+ );
+ } else {
+ var describe = "";
+ if (this.state.allow_valet === "false") {
+ describe = "Off";
+ } else {
+ describe = "On";
+ }
+
+ valetSection = (
+ <SettingItemMin
+ title="Valet"
+ describe={describe}
+ updateSection={function(){self.props.updateSection("valet");}}
+ />
+ );
+ }
+
+ return (
+ <div>
+ <div className="modal-header">
+ <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+ <h4 className="modal-title" ref="title"><i className="modal-back"></i>General Settings</h4>
+ </div>
+ <div ref="wrapper" className="user-settings">
+ <h3 className="tab-header">Feature Settings</h3>
+ <div className="divider-dark first"/>
+ {valetSection}
+ <div className="divider-dark"/>
+ </div>
+ </div>
+ );
+ }
+});
+
+module.exports = React.createClass({
+ componentDidMount: function() {
+ TeamStore.addChangeListener(this._onChange);
+ },
+ componentWillUnmount: function() {
+ TeamStore.removeChangeListener(this._onChange);
+ },
+ _onChange: function () {
+ var team = TeamStore.getCurrent();
+ if (!utils.areStatesEqual(this.state.team, team)) {
+ this.setState({ team: team });
+ }
+ },
+ getInitialState: function() {
+ return { team: TeamStore.getCurrent() };
+ },
+ render: function() {
+ if (this.props.activeTab === 'general') {
+ return (
+ <div>
+ </div>
+ );
+ } else if (this.props.activeTab === 'feature') {
+ return (
+ <div>
+ <FeatureTab team={this.state.team} activeSection={this.props.activeSection} updateSection={this.props.updateSection} />
+ </div>
+ );
+ } else {
+ return <div/>;
+ }
+ }
+});
diff --git a/web/react/components/settings_modal.jsx b/web/react/components/team_settings_modal.jsx
index 57a869f93..08a952d2e 100644
--- a/web/react/components/settings_modal.jsx
+++ b/web/react/components/team_settings_modal.jsx
@@ -2,7 +2,7 @@
// See License.txt for license information.
var SettingsSidebar = require('./settings_sidebar.jsx');
-var UserSettings = require('./user_settings.jsx');
+var TeamSettings = require('./team_settings.jsx');
module.exports = React.createClass({
componentDidMount: function() {
@@ -22,27 +22,31 @@ module.exports = React.createClass({
this.setState({ active_section: section });
},
getInitialState: function() {
- return { active_tab: "general", active_section: "" };
+ return { active_tab: "feature", active_section: "" };
},
render: function() {
+ var tabs = [];
+ tabs.push({name: "feature", ui_name: "Features", icon: "glyphicon glyphicon-wrench"});
+
return (
- <div className="modal fade" ref="modal" id="settings_modal" role="dialog" aria-hidden="true">
+ <div className="modal fade" ref="modal" id="team_settings" role="dialog" aria-hidden="true">
<div className="modal-dialog settings-modal">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
- <h4 className="modal-title" ref="title">Account Settings</h4>
+ <h4 className="modal-title" ref="title">Team Settings</h4>
</div>
<div className="modal-body">
<div className="settings-table">
<div className="settings-links">
<SettingsSidebar
+ tabs={tabs}
activeTab={this.state.active_tab}
updateTab={this.updateTab}
/>
</div>
<div className="settings-content">
- <UserSettings
+ <TeamSettings
activeTab={this.state.active_tab}
activeSection={this.state.active_section}
updateSection={this.updateSection}
diff --git a/web/react/components/user_settings.jsx b/web/react/components/user_settings.jsx
index 110634b50..7d542a8b7 100644
--- a/web/react/components/user_settings.jsx
+++ b/web/react/components/user_settings.jsx
@@ -658,9 +658,10 @@ var SecurityTab = React.createClass({
);
} else {
var d = new Date(this.props.user.last_password_update);
- var hour = d.getHours() < 10 ? "0" + d.getHours() : String(d.getHours());
+ var hour = d.getHours() % 12 ? String(d.getHours() % 12) : "12";
var min = d.getMinutes() < 10 ? "0" + d.getMinutes() : String(d.getMinutes());
- var dateStr = "Last updated " + Constants.MONTHS[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear() + " at " + hour + ":" + min;
+ var timeOfDay = d.getHours() >= 12 ? " pm" : " am";
+ var dateStr = "Last updated " + Constants.MONTHS[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear() + " at " + hour + ":" + min + timeOfDay;
passwordSection = (
<SettingItemMin
diff --git a/web/react/components/user_settings_modal.jsx b/web/react/components/user_settings_modal.jsx
new file mode 100644
index 000000000..ff001611d
--- /dev/null
+++ b/web/react/components/user_settings_modal.jsx
@@ -0,0 +1,68 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var SettingsSidebar = require('./settings_sidebar.jsx');
+var UserSettings = require('./user_settings.jsx');
+
+module.exports = React.createClass({
+ componentDidMount: function() {
+ $('body').on('click', '.modal-back', function(){
+ $(this).closest('.modal-dialog').removeClass('display--content');
+ });
+ $('body').on('click', '.modal-header .close', function(){
+ setTimeout(function() {
+ $('.modal-dialog.display--content').removeClass('display--content');
+ }, 500);
+ });
+ },
+ updateTab: function(tab) {
+ this.setState({ active_tab: tab });
+ },
+ updateSection: function(section) {
+ this.setState({ active_section: section });
+ },
+ getInitialState: function() {
+ return { active_tab: "general", active_section: "" };
+ },
+ render: function() {
+ var tabs = [];
+ tabs.push({name: "general", ui_name: "General", icon: "glyphicon glyphicon-cog"});
+ tabs.push({name: "security", ui_name: "Security", icon: "glyphicon glyphicon-lock"});
+ tabs.push({name: "notifications", ui_name: "Notifications", icon: "glyphicon glyphicon-exclamation-sign"});
+ tabs.push({name: "appearance", ui_name: "Appearance", icon: "glyphicon glyphicon-wrench"});
+ //tabs.push({name: "sessions", ui_name: "Sessions", icon: "glyphicon glyphicon-globe"});
+ //tabs.push({name: "activity_log", ui_name: "Activity Log", icon: "glyphicon glyphicon-time"});
+
+ return (
+ <div className="modal fade" ref="modal" id="user_settings1" role="dialog" aria-hidden="true">
+ <div className="modal-dialog settings-modal">
+ <div className="modal-content">
+ <div className="modal-header">
+ <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+ <h4 className="modal-title" ref="title">Account Settings</h4>
+ </div>
+ <div className="modal-body">
+ <div className="settings-table">
+ <div className="settings-links">
+ <SettingsSidebar
+ tabs={tabs}
+ activeTab={this.state.active_tab}
+ updateTab={this.updateTab}
+ />
+ </div>
+ <div className="settings-content">
+ <UserSettings
+ activeTab={this.state.active_tab}
+ activeSection={this.state.active_section}
+ updateSection={this.updateSection}
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+});
+
diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx
index 4cb30e1d3..c573e9dbb 100644
--- a/web/react/components/view_image.jsx
+++ b/web/react/components/view_image.jsx
@@ -124,7 +124,7 @@ module.exports = React.createClass({
<div key={name+"_loading"}>
<img ref="placeholder" className="loader-image" src="/static/images/load.gif" />
{ percentage > 0 ?
- <span className="loader-percent" >{"Downloading " + percentage + "%"}</span>
+ <span className="loader-percent" >{"Previewing " + percentage + "%"}</span>
: ""}
</div>
);
diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx
index df67d4360..3aa985863 100644
--- a/web/react/pages/channel.jsx
+++ b/web/react/pages/channel.jsx
@@ -22,7 +22,8 @@ var MoreChannelsModal = require('../components/more_channels.jsx');
var NewChannelModal = require('../components/new_channel.jsx');
var PostDeletedModal = require('../components/post_deleted_modal.jsx');
var ChannelNotificationsModal = require('../components/channel_notifications.jsx');
-var UserSettingsModal = require('../components/settings_modal.jsx');
+var UserSettingsModal = require('../components/user_settings_modal.jsx');
+var TeamSettingsModal = require('../components/team_settings_modal.jsx');
var ChannelMembersModal = require('../components/channel_members.jsx');
var ChannelInviteModal = require('../components/channel_invite_modal.jsx');
var TeamMembersModal = require('../components/team_members.jsx');
@@ -36,7 +37,7 @@ var ChannelInfoModal = require('../components/channel_info_modal.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
-global.window.setup_channel_page = function(team_name, team_type, channel_name, channel_id) {
+global.window.setup_channel_page = function(team_name, team_type, team_id, channel_name, channel_id) {
AppDispatcher.handleViewAction({
type: ActionTypes.CLICK_CHANNEL,
@@ -44,6 +45,11 @@ global.window.setup_channel_page = function(team_name, team_type, channel_name,
id: channel_id
});
+ AppDispatcher.handleViewAction({
+ type: ActionTypes.CLICK_TEAM,
+ id: team_id
+ });
+
React.render(
<ErrorBar/>,
document.getElementById('error_bar')
@@ -80,6 +86,11 @@ global.window.setup_channel_page = function(team_name, team_type, channel_name,
);
React.render(
+ <TeamSettingsModal />,
+ document.getElementById('team_settings_modal')
+ );
+
+ React.render(
<TeamMembersModal teamName={team_name} />,
document.getElementById('team_members_modal')
);
diff --git a/web/react/stores/team_store.jsx b/web/react/stores/team_store.jsx
new file mode 100644
index 000000000..e95daeeba
--- /dev/null
+++ b/web/react/stores/team_store.jsx
@@ -0,0 +1,100 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
+var EventEmitter = require('events').EventEmitter;
+var assign = require('object-assign');
+
+var Constants = require('../utils/constants.jsx');
+var ActionTypes = Constants.ActionTypes;
+
+
+var CHANGE_EVENT = 'change';
+
+var TeamStore = assign({}, EventEmitter.prototype, {
+ emitChange: function() {
+ this.emit(CHANGE_EVENT);
+ },
+ addChangeListener: function(callback) {
+ this.on(CHANGE_EVENT, callback);
+ },
+ removeChangeListener: function(callback) {
+ this.removeListener(CHANGE_EVENT, callback);
+ },
+ get: function(id) {
+ var c = this._getTeams();
+ return c[id];
+ },
+ getByName: function(name) {
+ var current = null;
+ var t = this._getTeams();
+
+ for (id in t) {
+ if (t[id].name == name) {
+ return t[id];
+ }
+ }
+
+ return null;
+ },
+ getAll: function() {
+ return this._getTeams();
+ },
+ setCurrentId: function(id) {
+ if (id == null)
+ sessionStorage.removeItem("current_team_id");
+ else
+ sessionStorage.setItem("current_team_id", id);
+ },
+ getCurrentId: function() {
+ return sessionStorage.getItem("current_team_id");
+ },
+ getCurrent: function() {
+ var currentId = TeamStore.getCurrentId();
+
+ if (currentId != null)
+ return this.get(currentId);
+ else
+ return null;
+ },
+ storeTeam: function(team) {
+ var teams = this._getTeams();
+ teams[team.id] = team;
+ this._storeTeams(teams);
+ },
+ _storeTeams: function(teams) {
+ sessionStorage.setItem("user_teams", JSON.stringify(teams));
+ },
+ _getTeams: function() {
+ var teams = {};
+
+ try {
+ teams = JSON.parse(sessionStorage.user_teams);
+ }
+ catch (err) {
+ }
+
+ return teams;
+ }
+});
+
+TeamStore.dispatchToken = AppDispatcher.register(function(payload) {
+ var action = payload.action;
+
+ switch(action.type) {
+
+ case ActionTypes.CLICK_TEAM:
+ TeamStore.setCurrentId(action.id);
+ TeamStore.emitChange();
+ break;
+
+ case ActionTypes.RECIEVED_TEAM:
+ TeamStore.storeTeam(action.team);
+ TeamStore.emitChange();
+ break;
+
+ default:
+ }
+});
+
+module.exports = TeamStore;
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index bb7ca458f..9383057c3 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -15,6 +15,8 @@ var ActionTypes = Constants.ActionTypes;
var callTracker = {};
var dispatchError = function(err, method) {
+ if (err.message === "There appears to be a problem with your internet connection") return;
+
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_ERROR,
err: err,
@@ -355,3 +357,25 @@ module.exports.getStatuses = function() {
}
);
}
+
+module.exports.getMyTeam = function() {
+ if (isCallInProgress("getMyTeam")) return;
+
+ callTracker["getMyTeam"] = utils.getTimestamp();
+ client.getMyTeam(
+ function(data, textStatus, xhr) {
+ callTracker["getMyTeam"] = 0;
+
+ if (xhr.status === 304 || !data) return;
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_TEAM,
+ team: data
+ });
+ },
+ function(err) {
+ callTracker["getMyTeam"] = 0;
+ dispatchError(err, "getMyTeam");
+ }
+ );
+}
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index 786e6dcea..15b6ace91 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -811,3 +811,34 @@ module.exports.getStatuses = function(success, error) {
}
});
};
+
+module.exports.getMyTeam = function(success, error) {
+ $.ajax({
+ url: "/api/v1/teams/me",
+ dataType: 'json',
+ type: 'GET',
+ success: success,
+ ifModified: true,
+ error: function(xhr, status, err) {
+ e = handleError("getMyTeam", xhr, status, err);
+ error(e);
+ }
+ });
+};
+
+module.exports.updateValetFeature = function(data, success, error) {
+ $.ajax({
+ url: "/api/v1/teams/update_valet_feature",
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success: success,
+ error: function(xhr, status, err) {
+ e = handleError("updateValetFeature", xhr, status, err);
+ error(e);
+ }
+ });
+
+ module.exports.track('api', 'api_teams_update_valet_feature');
+};
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index deb07409b..e5f42c8a0 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -27,6 +27,9 @@ module.exports = {
RECIEVED_STATUSES: null,
RECIEVED_MSG: null,
+
+ CLICK_TEAM: null,
+ RECIEVED_TEAM: null,
}),
PayloadSources: keyMirror({
@@ -45,6 +48,7 @@ module.exports = {
PATCH_TYPES: ['patch'],
ICON_FROM_TYPE: {'audio': 'audio', 'video': 'video', 'spreadsheet': 'ppt', 'pdf': 'pdf', 'code': 'code' , 'word': 'word' , 'excel': 'excel' , 'patch': 'patch', 'other': 'generic'},
MAX_DISPLAY_FILES: 5,
+ MAX_UPLOAD_FILES: 5,
MAX_FILE_SIZE: 50000000, // 50 MB
DEFAULT_CHANNEL: 'town-square',
POST_CHUNK_SIZE: 60,
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index fb4f3a34e..75c583c8f 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -96,6 +96,21 @@ module.exports.getCookie = function(name) {
if (parts.length == 2) return parts.pop().split(";").shift();
}
+module.exports.isLocalStorageSupported = function() {
+ try {
+ sessionStorage.setItem("testSession", '1');
+ sessionStorage.removeItem("testSession");
+
+ localStorage.setItem("testLocal", '1');
+ localStorage.removeItem("testLocal", '1');
+
+ return true;
+ }
+ catch (e) {
+ return false;
+ }
+}
+
module.exports.notifyMe = function(title, body, channel) {
if ("Notification" in window && Notification.permission !== 'denied') {
Notification.requestPermission(function (permission) {
@@ -418,7 +433,7 @@ module.exports.textToJsx = function(text, options) {
highlightSearchClass = " search-highlight";
}
- inner.push(<span key={name+i+z+"_span"}>{prefix}<a className={mClass + highlightSearchClass + " mention-link"} key={name+i+z+"_link"} href="#" onClick={function() {module.exports.searchForTerm(name);}}>@{name}</a>{suffix} </span>);
+ inner.push(<span key={name+i+z+"_span"}>{prefix}<a className={mClass + highlightSearchClass + " mention-link"} key={name+i+z+"_link"} href="#" onClick={function(value) { return function() { module.exports.searchForTerm(value); } }(name)}>@{name}</a>{suffix} </span>);
} else if (urlMatcher.test(word)) {
var match = urlMatcher.match(word)[0];
var link = match.url;
@@ -431,7 +446,7 @@ module.exports.textToJsx = function(text, options) {
} else if (trimWord.match(hashRegex)) {
var suffix = word.match(puncEndRegex);
var prefix = word.match(puncStartRegex);
- var mClass = trimWord in implicitKeywords ? mentionClass : "";
+ var mClass = trimWord in implicitKeywords || trimWord.toLowerCase() in implicitKeywords ? mentionClass : "";
if (searchTerm === trimWord.substring(1).toLowerCase() || searchTerm === trimWord.toLowerCase()) {
highlightSearchClass = " search-highlight";
@@ -439,7 +454,7 @@ module.exports.textToJsx = function(text, options) {
inner.push(<span key={word+i+z+"_span"}>{prefix}<a key={word+i+z+"_hash"} className={"theme " + mClass + highlightSearchClass} href="#" onClick={function(value) { return function() { module.exports.searchForTerm(value); } }(trimWord)}>{trimWord}</a>{suffix} </span>);
- } else if (trimWord in implicitKeywords) {
+ } else if (trimWord in implicitKeywords || trimWord.toLowerCase() in implicitKeywords) {
var suffix = word.match(puncEndRegex);
var prefix = word.match(puncStartRegex);
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index 6da516cf9..745d50173 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -42,49 +42,63 @@ body.ios {
}
#post-list {
- .new-seperator {
+ .date-separator, .new-separator {
text-align: center;
- padding: 1em 0;
- .new-seperator__hr {
- border-color: #FFAF53;
- margin: 0;
+ height: 2em;
+ margin: 0;
+ position: relative;
+ &:before, &:after {
+ content: "";
+ height: 1em;
+ position: absolute;
+ left: 0;
+ width: 100%;
+ display: none;
}
- .new-seperator__text {
- margin-top: -11px;
- color: #FF8800;
- background: #FFF;
- display: inline-block;
- padding: 0 1em;
- font-weight: normal;
+ &:before {
+ bottom: 0;
+ }
+ &:after {
+ top: 0;
}
- }
- .date-seperator {
- text-align: center;
- margin: 1em 0;
- height: 0;
&.hovered--after {
- margin-bottom: 0;
- padding-bottom: 1em;
- background: #f6f6f6;
+ &:before {
+ background: #f6f6f6;
+ display: block;
+ }
}
&.hovered--before {
- margin-top: 0;
- padding-top: 1em;
- background: #f6f6f6;
+ &:after {
+ background: #f6f6f6;
+ display: block;
+ }
}
- .date-seperator__hr {
+ .separator__hr {
border-color: #ccc;
margin: 0;
+ position: relative;
+ z-index: 5;
+ top: 1em;
}
- .date-seperator__text {
- margin-top: -13px;
- line-height: 24px;
+ .separator__text {
+ line-height: 2em;
color: #555;
background: #FFF;
display: inline-block;
padding: 0 1em;
font-weight: 900;
@include border-radius(50px);
+ position: relative;
+ z-index: 5;
+ }
+ }
+ .new-separator {
+ .separator__hr {
+ border-color: #FFAF53;
+ }
+ .separator__text {
+ color: #F80;
+ font-weight: normal;
}
}
.post-list-holder-by-time {
diff --git a/web/static/config/config.js b/web/static/config/config.js
index 82d4bbf70..45c713da2 100644
--- a/web/static/config/config.js
+++ b/web/static/config/config.js
@@ -13,6 +13,7 @@ var config = {
// Feature switches
AllowPublicLink: true,
AllowInviteNames: true,
+ RequireInviteNames: false,
AllowSignupDomainsWizard: false,
// Privacy switches
diff --git a/web/templates/channel.html b/web/templates/channel.html
index d313b5395..d10ae2304 100644
--- a/web/templates/channel.html
+++ b/web/templates/channel.html
@@ -26,6 +26,7 @@
<div id="edit_mention_tab"></div>
<div id="get_link_modal"></div>
<div id="user_settings_modal"></div>
+ <div id="team_settings_modal"></div>
<div id="invite_member_modal"></div>
<div id="edit_channel_modal"></div>
<div id="delete_channel_modal"></div>
@@ -43,7 +44,7 @@
<div id="direct_channel_modal"></div>
<div id="channel_info_modal"></div>
<script>
-window.setup_channel_page('{{ .Props.TeamName }}', '{{ .Props.TeamType }}', '{{ .Props.ChannelName }}', '{{ .Props.ChannelId }}');
+window.setup_channel_page('{{ .Props.TeamName }}', '{{ .Props.TeamType }}', '{{ .Props.TeamId }}', '{{ .Props.ChannelName }}', '{{ .Props.ChannelId }}');
</script>
</body>
</html>
diff --git a/web/web.go b/web/web.go
index 3210ede1e..7357124b5 100644
--- a/web/web.go
+++ b/web/web.go
@@ -319,6 +319,7 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
page.Title = name + " - " + team.Name + " " + page.SiteName
page.Props["TeamName"] = team.Name
page.Props["TeamType"] = team.Type
+ page.Props["TeamId"] = team.Id
page.Props["ChannelName"] = name
page.Props["ChannelId"] = channelId
page.Props["UserId"] = c.Session.UserId