Merge branch 'master' into PLT-12-log

Этот коммит содержится в:
=Corey Hulen
2015-09-14 16:59:24 -07:00
родитель e06e292be7 bfebb41bc0
Коммит 3bd350dcaf
59 изменённых файлов: 18560 добавлений и 731 удалений

Просмотреть файл

@@ -21,6 +21,8 @@ Documentation
- Updated software and hardware requirements documentation
- Re-organized install instructions out of README and into separate files
- Added Code Contribution Guidelines
- Added new hardware sizing recommendations
- Consolidated licensing information into LICENSE.txt and NOTICE.txt
Performance
@@ -33,7 +35,8 @@ Code Quality
### Bug Fixes
- Fixed performance issue with slow typing on iOS
- Fixed performance issue with slow typing on iOS
- Fixed issue so that SSO option automatically set EmailVerified=true (it was false previously)
### Contributors

Просмотреть файл

@@ -78,9 +78,10 @@ travis:
mv $(DIST_PATH)/web/static/js/bundle.min.js $(DIST_PATH)/web/static/js/bundle-$(BUILD_NUMBER).min.js
@sed -i'.bak' 's|react-with-addons-0.13.1.js|react-with-addons-0.13.1.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|react-with-addons-0.13.3.js|react-with-addons-0.13.3.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|jquery-1.11.1.js|jquery-1.11.1.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|bootstrap-3.3.1.js|bootstrap-3.3.1.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|react-bootstrap-0.25.1.js|react-bootstrap-0.25.1.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|perfect-scrollbar.js|perfect-scrollbar.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
rm $(DIST_PATH)/web/templates/*.bak
@@ -231,9 +232,10 @@ dist: install
mv $(DIST_PATH)/web/static/js/bundle.min.js $(DIST_PATH)/web/static/js/bundle-$(BUILD_NUMBER).min.js
@sed -i'.bak' 's|react-with-addons-0.13.1.js|react-with-addons-0.13.1.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|react-with-addons-0.13.3.js|react-with-addons-0.13.3.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|jquery-1.11.1.js|jquery-1.11.1.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|bootstrap-3.3.1.js|bootstrap-3.3.1.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|react-bootstrap-0.25.1.js|react-bootstrap-0.25.1.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|perfect-scrollbar.js|perfect-scrollbar.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
rm $(DIST_PATH)/web/templates/*.bak

Просмотреть файл

@@ -2,7 +2,7 @@
Mattermost is an open source, on-prem Slack-alternative.
It modernizes team communication without locking in your data to a single provider. Offer your end users messaging and file sharing across PCs and phones with archiving and instant search--without losing control of your data.
It offers modern communication from behind your firewall, including messaging and file sharing across PCs and phones with archiving and instant search.
## All team communication in one place, searchable and accessible anywhere
@@ -26,12 +26,6 @@ Please see the [features pages of the Mattermost website](http://www.mattermost.
- Attach sound, video and image files from mobile devices
- Define team-specific branding and color themes across your devices
#### Enterprise Compatible
- Self-host Mattermost entirely within your organization's insfrastructure
- GitLab Mattermost omnibus supports install to over 100,000 organizations using GitLab
- LDAP/Active Directory connectivity to Mattermost enabled via GitLab Single-Sign-On
## Learn More
- [Product Vision and Target Audiences](http://www.mattermost.org/vision/) - What we're solving and for whom are we building
@@ -48,9 +42,9 @@ There are multiple ways to install Mattermost depending on your needs.
#### Quick Start Install for Product Evaluation
- [Local Machine Install and Upgrade with Docker](doc/install/single-container-install.md) - Explore product functionality using a single-container Docker install on a local machine, including Mac OSX, Ubuntu, or Arch Linux). Optionally set up email and upgrade your instance using DockerHub.
- [Local Machine Install with Docker](doc/install/single-container-install.md) - Explore product functionality using a single-container Docker install on a local machine, including Mac OSX, Ubuntu, or Arch Linux). Optionally set up email and upgrade your instance using DockerHub.
- [AWS EBS Install and Upgrade with Docker](doc/install/aws-ebs-setup.md) - Explore product functionality using a single-container Docker install for Amazon Web Services Elastic Beanstalk. Optionally set up email and upgrade your instance using DockerHub.
- [AWS EBS Install with Docker](doc/install/aws-ebs-setup.md) - Explore product functionality using a single-container Docker install for Amazon Web Services Elastic Beanstalk. Optionally set up email and upgrade your instance using DockerHub.
#### Development Install
@@ -63,3 +57,38 @@ Prior to production installation, please review [Mattermost system requirements]
- [GitLab Mattermost Production Installation](https://gitlab.com/gitlab-org/gitlab-mattermost) - Install Mattermost for production environments bundled with GitLab, a leading open source Git repository, using an omnibus package for Ubuntu 12.04, Ubuntu 14.04, Debian 7, Debian 8, and CentOS 6 (and RedHat/Oracle/Scientific Linux 6), CentOS 7 (and RedHat/Oracle/Scientific Linux 7).
For technical questions and answers, please visit the [Mattermost forum](http://forum.mattermost.org).
## Get Involved with Mattermost
Joining the Mattermost community is a great way to build relationships with other talented and like-minded professionals, increase awareness of the interesting work you are doing, and sharpen your skills. Here are some of the ways that you can make a difference in the Mattermost ecosystem:
#### Discuss
- Visit the [Mattermost Forum](http://forum.mattermost.org/) for technical questions and answers.
- [Join](https://groups.google.com/a/mattermost.com/forum/#!forum/general/join) the [Mattermost User Mailing List](https://groups.google.com/a/mattermost.com/forum/#!forum/general) for general, open-ended questions and software announcements.
- [Join](https://groups.google.com/a/mattermost.com/forum/#!forum/developer/join) the [Mattermost Developers Mailing List](https://groups.google.com/a/mattermost.com/forum/#!forum/developer) for discussing contributing code to Mattermost.
#### Review
- Share feedback on [Mattermost Design Feedback Requests](http://forum.mattermost.org/c/feature-ideas/specifications), which offer early previews of designs community comments and feedback.
- Visit the [Feature Ideas Forum](http://mattermost.uservoice.com/forums/306457-general) and consider upvoting community feature ideas, which are reviewed for each monthly release.
#### Share
- Blog about your Mattermost experiences and use cases, either on your personal blog, the [Mattermost forum](http://forum.mattermost.org), or contribute a guest post to the [Mattermost blog](http://www.mattermost.org/category/blog/). If you write about Mattermost. please contact our community team at info@mattermost.com for help promoting your content.
- Tweet to share with your community and friends why you use Mattermost. Follow [@MattermostHQ](https://twitter.com/mattermosthq) on Twitter and conversations on [#mattermost](https://twitter.com/search?q=%23mattermost&src=typd).
#### Write
- As with most open source projects, Mattermost documentation is maintained in a public repository. You can propose changes by [submitting pull requests (no programming skills required)](http://forum.mattermost.org/t/help-improve-mattermost-documentation/194). We highly welcome you to add improvements, write guides and tutorials, and expand on sections.
- Prior to contributing, please review [Mattermost Documentation Guidelines](http://www.mattermost.org/documentation-guidelines/), which include standards on writing Mattermost documentation for a global audience, who might not use English as their first language.
#### Contribute
- Share [feature ideas](http://www.mattermost.org/feature-requests/) with the Mattermost community
- Review the [Mattermost Code Contribution Guidelines](doc/developer/code-contribution.md) to submit patches for the core product
- Consider building tools that help developers and IT professionals manage Mattermost more effectively (API documentation coming in Beta2)
#### Have other ideas or suggestions?
If theres some other way youd like to contribute, please contact us at info@mattermost.com. Wed love to meet you!

Просмотреть файл

@@ -191,7 +191,7 @@ func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) {
if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_TEAM_ADMIN) {
c.Err = model.NewAppError("updateChannel", "You do not have the appropriate permissions", "")
c.Err.StatusCode = http.StatusForbidden
return
@@ -514,7 +514,7 @@ func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) {
if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_TEAM_ADMIN) {
c.Err = model.NewAppError("deleteChannel", "You do not have the appropriate permissions", "")
c.Err.StatusCode = http.StatusForbidden
return
@@ -756,7 +756,7 @@ func removeChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) {
if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_TEAM_ADMIN) {
c.Err = model.NewAppError("updateChannel", "You do not have the appropriate permissions ", "")
c.Err.StatusCode = http.StatusForbidden
return

Просмотреть файл

@@ -106,6 +106,9 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !h.isApi {
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Content-Security-Policy", "frame-ancestors none")
} else {
// All api response bodies will be JSON formatted
w.Header().Set("Content-Type", "application/json")
}
sessionId := ""
@@ -308,7 +311,7 @@ func (c *Context) IsTeamAdmin(userId string) bool {
return false
} else {
user := uresult.Data.(*model.User)
return model.IsInRole(c.Session.Roles, model.ROLE_ADMIN) && user.TeamId == c.Session.TeamId
return model.IsInRole(c.Session.Roles, model.ROLE_TEAM_ADMIN) && user.TeamId == c.Session.TeamId
}
}

Просмотреть файл

@@ -349,6 +349,7 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "max-age=2592000, public")
w.Header().Set("Content-Length", strconv.Itoa(len(f)))
w.Header().Set("Content-Type", "") // need to provide proper Content-Type in the future
w.Write(f)
}

Просмотреть файл

@@ -716,7 +716,7 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if post.UserId != c.Session.UserId && !model.IsInRole(c.Session.Roles, model.ROLE_ADMIN) {
if post.UserId != c.Session.UserId && !model.IsInRole(c.Session.Roles, model.ROLE_TEAM_ADMIN) {
c.Err = model.NewAppError("deletePost", "You do not have the appropriate permissions", "")
c.Err.StatusCode = http.StatusForbidden
return

Просмотреть файл

@@ -50,6 +50,15 @@ func SlackConvertTimeStamp(ts string) int64 {
return timeStamp * 1000 // Convert to milliseconds
}
func SlackConvertChannelName(channelName string) string {
newName := strings.Trim(channelName, "_-")
if len(newName) == 1 {
return "slack-channel-" + newName
}
return newName
}
func SlackParseChannels(data io.Reader) []SlackChannel {
decoder := json.NewDecoder(data)
@@ -172,7 +181,7 @@ func SlackAddChannels(teamId string, slackchannels []SlackChannel, posts map[str
TeamId: teamId,
Type: model.CHANNEL_OPEN,
DisplayName: sChannel.Name,
Name: sChannel.Name,
Name: SlackConvertChannelName(sChannel.Name),
Description: sChannel.Topic["value"],
}
mChannel := ImportChannel(&newChannel)

Просмотреть файл

@@ -477,7 +477,7 @@ func InviteMembers(c *Context, team *model.Team, user *model.User, invites []str
sender := user.GetDisplayName()
senderRole := ""
if model.IsInRole(user.Roles, model.ROLE_ADMIN) || model.IsInRole(user.Roles, model.ROLE_SYSTEM_ADMIN) {
if model.IsInRole(user.Roles, model.ROLE_TEAM_ADMIN) || model.IsInRole(user.Roles, model.ROLE_SYSTEM_ADMIN) {
senderRole = "administrator"
} else {
senderRole = "member"
@@ -536,7 +536,7 @@ func updateTeamDisplayName(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if !model.IsInRole(c.Session.Roles, model.ROLE_ADMIN) {
if !model.IsInRole(c.Session.Roles, model.ROLE_TEAM_ADMIN) {
c.Err = model.NewAppError("updateTeamDisplayName", "You do not have the appropriate permissions", "userId="+c.Session.UserId)
c.Err.StatusCode = http.StatusForbidden
return
@@ -576,7 +576,7 @@ func updateValetFeature(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if !model.IsInRole(c.Session.Roles, model.ROLE_ADMIN) {
if !model.IsInRole(c.Session.Roles, model.ROLE_TEAM_ADMIN) {
c.Err = model.NewAppError("updateValetFeature", "You do not have the appropriate permissions", "userId="+c.Session.UserId)
c.Err.StatusCode = http.StatusForbidden
return

Просмотреть файл

@@ -170,7 +170,7 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User {
channelRole := ""
if team.Email == user.Email {
user.Roles = model.ROLE_ADMIN
user.Roles = model.ROLE_TEAM_ADMIN
channelRole = model.CHANNEL_ROLE_ADMIN
} else {
user.Roles = ""
@@ -945,7 +945,7 @@ func updateRoles(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if !model.IsInRole(c.Session.Roles, model.ROLE_ADMIN) && !c.IsSystemAdmin() {
if !model.IsInRole(c.Session.Roles, model.ROLE_TEAM_ADMIN) && !c.IsSystemAdmin() {
c.Err = model.NewAppError("updateRoles", "You do not have the appropriate permissions", "userId="+user_id)
c.Err.StatusCode = http.StatusForbidden
return
@@ -984,7 +984,7 @@ func UpdateRoles(c *Context, user *model.User, roles string) *model.User {
// make sure there is at least 1 other active admin
if !model.IsInRole(roles, model.ROLE_SYSTEM_ADMIN) {
if model.IsInRole(user.Roles, model.ROLE_ADMIN) && !model.IsInRole(roles, model.ROLE_ADMIN) {
if model.IsInRole(user.Roles, model.ROLE_TEAM_ADMIN) && !model.IsInRole(roles, model.ROLE_TEAM_ADMIN) {
if result := <-Srv.Store.User().GetProfiles(user.TeamId); result.Err != nil {
c.Err = result.Err
return nil
@@ -992,7 +992,7 @@ func UpdateRoles(c *Context, user *model.User, roles string) *model.User {
activeAdmins := -1
profileUsers := result.Data.(map[string]*model.User)
for _, profileUser := range profileUsers {
if profileUser.DeleteAt == 0 && model.IsInRole(profileUser.Roles, model.ROLE_ADMIN) {
if profileUser.DeleteAt == 0 && model.IsInRole(profileUser.Roles, model.ROLE_TEAM_ADMIN) {
activeAdmins = activeAdmins + 1
}
}
@@ -1042,14 +1042,14 @@ func updateActive(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if !model.IsInRole(c.Session.Roles, model.ROLE_ADMIN) && !c.IsSystemAdmin() {
if !model.IsInRole(c.Session.Roles, model.ROLE_TEAM_ADMIN) && !c.IsSystemAdmin() {
c.Err = model.NewAppError("updateActive", "You do not have the appropriate permissions", "userId="+user_id)
c.Err.StatusCode = http.StatusForbidden
return
}
// make sure there is at least 1 other active admin
if !active && model.IsInRole(user.Roles, model.ROLE_ADMIN) {
if !active && model.IsInRole(user.Roles, model.ROLE_TEAM_ADMIN) {
if result := <-Srv.Store.User().GetProfiles(user.TeamId); result.Err != nil {
c.Err = result.Err
return
@@ -1057,7 +1057,7 @@ func updateActive(c *Context, w http.ResponseWriter, r *http.Request) {
activeAdmins := -1
profileUsers := result.Data.(map[string]*model.User)
for _, profileUser := range profileUsers {
if profileUser.DeleteAt == 0 && model.IsInRole(profileUser.Roles, model.ROLE_ADMIN) {
if profileUser.DeleteAt == 0 && model.IsInRole(profileUser.Roles, model.ROLE_TEAM_ADMIN) {
activeAdmins = activeAdmins + 1
}
}

Просмотреть файл

@@ -509,7 +509,7 @@ func TestUserUpdate(t *testing.T) {
user.TeamId = "12345678901234567890123456"
user.LastActivityAt = time2
user.LastPingAt = time2
user.Roles = model.ROLE_ADMIN
user.Roles = model.ROLE_TEAM_ADMIN
user.LastPasswordUpdate = 123
if result, err := Client.UpdateUser(user); err != nil {

Просмотреть файл

@@ -16,9 +16,10 @@
### Configuration
- [GitLab SSO Configuration](integrations/sso/gitlab-sso.md)
- [SMTP Email Setup](smtp-email-setup.md)
- [SMTP Email Setup](config/smtp-email-setup.md)
## Developer Documentation
- [Developer Machine Setup](doc/install/dev-setup.md)
- [Code Contribution Guidelines](developer/code-contribution.md)
- [Developer Machine Setup](install/dev-setup.md)
- [Mattermost Style Guide](developer/style-guide.md)

44
doc/developer/code-contribution.md Обычный файл
Просмотреть файл

@@ -0,0 +1,44 @@
# Code Contribution Guidelines
Thank you for your interest in contributing to Mattermost. This guide provides an overview of important information for contributors to know.
## Choose a Ticket
1. Review the list of [Good First Contribution](https://mattermost.atlassian.net/issues/?filter=10206) tickets listed in Jira.
2. These projects are intended to be a straight forward first pull requests from new contributors.
If you don't find something appropriate for your interests, please see the full list of tickets [Accepting Pull Requests](https://mattermost.atlassian.net/issues/?filter=10101).
3. If you have any questions at all about a ticket, please post to the [Contributor Discussion section](http://forum.mattermost.org/) of the Mattermost forum, or email the [Mattermost Developer Mailing list](https://groups.google.com/a/mattermost.com/forum/#!forum/developer/join).
## Install Mattermost and set up a Fork
1. Follow [developer setup instructions](https://github.com/mattermost/platform/blob/master/doc/install/dev-setup.md) to install Mattermost.
2. Create a branch with <branch name> set to the ID of the ticket you're working on, for example ```PLT-394```, using command:
```
git checkout -b <branch name>
```
## Programming and Testing
1. Please review the [Mattermost Style Guide](developer/style-guide.md) prior to making changes.
2. Please make sure to thoroughly test your change before submitting a pull request.
## Submitting a Pull Request
1. Please add yourself to the contributor list prior to submitting by completing the [contributor license agreement](http://www.mattermost.org/mattermost-contributor-agreement/).
2. When you submit your pull request please include the Ticket ID at the beginning of your pull request comment, followed by a colon.
For example, for a ticket ID `PLT-394` start your comment with: `PLT-394:`. See [previously closed pull requests](https://github.com/mattermost/platform/pulls?q=is%3Apr+is%3Aclosed) for examples.
3. Once submitted, your pull request will be checked via an automated build process and will be reviewed by the Mattermost core team, who may either accept the PR or follow-up with feedback.
4. If you've included your mailing address in Step 1, you'll be receiving a [Limited Edition Mattermost Mug](http://forum.mattermost.org/t/limited-edition-mattermost-mugs/143) as a thank you gift after your first pull request has been accepted.

Просмотреть файл

@@ -10,7 +10,7 @@ Developer Machine Setup
2. Get your Docker IP address with `boot2docker ip`
3. Add a line to your /etc/hosts that goes `<Docker IP> dockerhost`
4. Run `boot2docker shellinit` and copy the export statements to your ~/.bash_profile
2. Download Go from http://golang.org/dl/
2. Download Go (version 1.4.2) from http://golang.org/dl/
3. Set up your Go workspace
1. `mkdir ~/go`
2. Add the following to your ~/.bash_profile
@@ -22,13 +22,13 @@ Developer Machine Setup
1. Download Homebrew from http://brew.sh/
2. `brew install node`
5. Install Compass
1. Make sure you have the latest verison of Ruby
2. `gem install compass`
1. Run `ruby -v` and check the ruby version is 1.8.7 or higher
2. `sudo gem install compass`
6. Download Mattermost
`cd ~/go`
`mkdir -p src/github.com/mattermost`
`cd src/github.com/mattermost`
`git clone github.com/mattermost/platform.git`
`git clone https://github.com/mattermost/platform.git`
`cd platform`
7. Run unit tests on Mattermost using `make test` to make sure the installation was successful
8. If tests passed, you can now run Mattermost using `make run`
@@ -50,7 +50,7 @@ Any issues? Please let us know on our forums at: http://forum.mattermost.org
`127.0.0.1 dockerhost`
3. Install build essentials
1. `apt-get install build-essential`
4. Download Go from http://golang.org/dl/
4. Download Go (version 1.4.2) from http://golang.org/dl/
5. Set up your Go workspace and add Go to the PATH
1. `mkdir ~/go`
2. Add the following to your ~/.bashrc
@@ -74,7 +74,7 @@ Any issues? Please let us know on our forums at: http://forum.mattermost.org
`cd ~/go`
`mkdir -p src/github.com/mattermost`
`cd src/github.com/mattermost`
`git clone github.com/mattermost/platform.git`
`git clone https://github.com/mattermost/platform.git`
`cd platform`
9. Run unit tests on Mattermost using `make test` to make sure the installation was successful
10. If tests passed, you can now run Mattermost using `make run`

Просмотреть файл

@@ -1,28 +1,13 @@
**Mattermost Alpha**
**Team Communication Service**
**Development Build**
Mattermost
==========
About Mattermost
================
http:/mattermost.org
Mattermost is an open-source team communication service. It brings team messaging and file sharing into one place, accessible across PCs and phones, with archiving and search.
Installing Mattermost
=====================
You're installing "Mattermost Alpha", a pre-released version providing an early look at what we're building. While the core team runs this version internally, it's not recommended for production since we can't guarantee API stability or backwards compatibility.
That said, any issues at all, please let us know on the Mattermost forum.
Notes:
- For Alpha, Docker is intentionally setup as a single container, since production deployment is not yet recommended.
Local Machine Setup (Docker)
-----------------------------
To run an instance of the latest version of mattermost on your local machine you can run:
`docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform`
@@ -35,9 +20,4 @@ To run an instance of the latest code from the master branch on GitHub you can r
`docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:dev`
License
-------
Mattermost is licensed under an "Apache-wrapped AGPL" model inspired by MongoDB. Similar to MongoDB, you can run and link to the system using Configuration Files and Admin Tools licensed under Apache, version 2.0, as described in the LICENSE file, as an explicit exception to the terms of the GNU Affero General Public License (AGPL) that applies to most of the remaining source files. See individual files for details.
Any questions, please visit http://forum.mattermost.org

Просмотреть файл

@@ -328,9 +328,9 @@ Usage:
Example:
platform -create_user -team_name="name" -email="user@example.com" -password="mypassword"
-assign_role Assigns role to a user. It requres the -team_name,
-email and -role flag. If you're assigning the
"system_admin" role it must be for a user on the
-assign_role Assigns role to a user. It requres the -role,
-email and -team_name flag. If you're assigning the
role="system_admin" role it must be for a user on the
team_name="admin"
Example:
platform -assign_role -team_name="name" -email="user@example.com" -role="admin"

Просмотреть файл

@@ -158,7 +158,7 @@ func IsReservedTeamName(s string) bool {
func IsValidTeamName(s string) bool {
if !IsValidAlphaNum(s) {
if !IsValidAlphaNum(s, false) {
return false
}

Просмотреть файл

@@ -13,7 +13,7 @@ import (
)
const (
ROLE_ADMIN = "admin"
ROLE_TEAM_ADMIN = "admin"
ROLE_SYSTEM_ADMIN = "system_admin"
USER_AWAY_TIMEOUT = 5 * 60 * 1000 // 5 minutes
USER_OFFLINE_TIMEOUT = 1 * 60 * 1000 // 1 minute
@@ -289,7 +289,7 @@ func isValidRole(role string) bool {
return true
}
if role == ROLE_ADMIN {
if role == ROLE_TEAM_ADMIN {
return true
}

Просмотреть файл

@@ -202,7 +202,7 @@ func GetSubDomain(s string) (string, string) {
func IsValidChannelIdentifier(s string) bool {
if !IsValidAlphaNum(s) {
if !IsValidAlphaNum(s, true) {
return false
}
@@ -213,10 +213,16 @@ func IsValidChannelIdentifier(s string) bool {
return true
}
var validAlphaNumUnderscore = regexp.MustCompile(`^[a-z0-9]+([a-z\-\_0-9]+|(__)?)[a-z0-9]+$`)
var validAlphaNum = regexp.MustCompile(`^[a-z0-9]+([a-z\-0-9]+|(__)?)[a-z0-9]+$`)
func IsValidAlphaNum(s string) bool {
match := validAlphaNum.MatchString(s)
func IsValidAlphaNum(s string, allowUnderscores bool) bool {
var match bool
if allowUnderscores {
match = validAlphaNumUnderscore.MatchString(s)
} else {
match = validAlphaNum.MatchString(s)
}
if !match {
return false

Просмотреть файл

@@ -74,7 +74,7 @@ func (us SqlUserStore) Save(user *model.User) StoreChannel {
close(storeChannel)
return
} else if int(count) > utils.Cfg.TeamSettings.MaxUsersPerTeam {
result.Err = model.NewAppError("SqlUserStore.Save", "You've reached the limit of the number of allowed accounts.", "teamId="+user.TeamId)
result.Err = model.NewAppError("SqlUserStore.Save", "This team has reached the maxmium number of allowed accounts. Contact your systems administrator to set a higher limit.", "teamId="+user.TeamId)
storeChannel <- result
close(storeChannel)
return

Просмотреть файл

@@ -141,17 +141,14 @@ var FUZZY_STRINGS_NAMES = []string{
// Strings that should pass as acceptable emails
var FUZZY_STRINGS_EMAILS = []string{
"sue@thatmightbe",
"sue@thatmightbe.",
"sue@thatmightbe.c",
"sue@thatmightbe.co",
"su+san@thatmightbe.com",
"a@b.中国",
"1@2.am",
"a@b.co.uk",
"a@b.cancerresearch",
"su+s+an@thatmightbe.com",
"per.iod@thatmightbe.com",
"per..iods@thatmightbe.com",
}
// Lovely giberish for all to use

Просмотреть файл

@@ -18,7 +18,8 @@
"es6": true
},
"globals": {
"React": false
"React": false,
"ReactBootstrap": false
},
"rules": {
"comma-dangle": [2, "never"],

177
web/react/components/change_url_modal.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,177 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Modal = ReactBootstrap.Modal;
var Utils = require('../utils/utils.jsx');
export default class ChangeUrlModal extends React.Component {
constructor(props) {
super(props);
this.onURLChanged = this.onURLChanged.bind(this);
this.doSubmit = this.doSubmit.bind(this);
this.doCancel = this.doCancel.bind(this);
this.state = {
currentURL: props.currentURL,
urlError: '',
userEdit: false
};
}
componentWillReceiveProps(nextProps) {
// This check prevents the url being deleted when we re-render
// because of user status check
if (!this.state.userEdit) {
this.setState({
currentURL: nextProps.currentURL
});
}
}
componentDidUpdate(prevProps) {
if (this.props.show === true && prevProps.show === false) {
React.findDOMNode(this.refs.urlinput).select();
}
}
onURLChanged(e) {
const url = e.target.value.trim();
this.setState({currentURL: url.replace(/[^A-Za-z0-9-_]/g, '').toLowerCase(), userEdit: true});
}
getURLError(url) {
let error = []; //eslint-disable-line prefer-const
if (url.length < 2) {
error.push(<span key='error1'>{'Must be longer than two characters'}<br/></span>);
}
if (url.charAt(0) === '-' || url.charAt(0) === '_') {
error.push(<span key='error2'>{'Must start with a letter or number'}<br/></span>);
}
if (url.length > 1 && (url.charAt(url.length - 1) === '-' || url.charAt(url.length - 1) === '_')) {
error.push(<span key='error3'>{'Must end with a letter or number'}<br/></span>);
}
if (url.indexOf('__') > -1) {
error.push(<span key='error4'>{'Can not contain two underscores in a row.'}<br/></span>);
}
// In case of error we don't detect
if (error.length === 0) {
error.push(<span key='errorlast'>{'Invalid URL'}<br/></span>);
}
return error;
}
doSubmit(e) {
e.preventDefault();
const url = React.findDOMNode(this.refs.urlinput).value;
const cleanedURL = Utils.cleanUpUrlable(url);
if (cleanedURL !== url || url.length < 2 || url.indexOf('__') > -1) {
this.setState({urlError: this.getURLError(url)});
return;
}
this.setState({urlError: '', userEdit: false});
this.props.onModalSubmit(url);
}
doCancel() {
this.setState({urlError: '', userEdit: false});
this.props.onModalDismissed();
}
render() {
let urlClass = 'input-group input-group--limit';
let urlError = null;
let serverError = null;
if (this.state.urlError) {
urlClass += ' has-error';
urlError = (<p className='input__help error'>{this.state.urlError}</p>);
}
if (this.props.serverError) {
serverError = <div className='form-group has-error'><p className='input__help error'>{this.props.serverError}</p></div>;
}
const fullTeamUrl = Utils.getTeamURLFromAddressBar();
const teamURL = Utils.getShortenedTeamURL();
return (
<Modal
show={this.props.show}
onHide={this.doCancel}
>
<Modal.Header closeButton={true}>
<Modal.Title>{this.props.title}</Modal.Title>
</Modal.Header>
<form
role='form'
className='form-horizontal'
>
<Modal.Body>
<div className='modal-intro'>{this.props.description}</div>
<div className='form-group'>
<label className='col-sm-2 form__label control-label'>{this.props.urlLabel}</label>
<div className='col-sm-10'>
<div className={urlClass}>
<span
data-toggle='tooltip'
title={fullTeamUrl}
className='input-group-addon'
>
{teamURL}
</span>
<input
type='text'
ref='urlinput'
className='form-control'
maxLength='22'
onChange={this.onURLChanged}
value={this.state.currentURL}
autoFocus={true}
tabIndex='1'
/>
</div>
{urlError}
{serverError}
</div>
</div>
</Modal.Body>
<Modal.Footer>
<button
type='button'
className='btn btn-default'
onClick={this.doCancel}
>
{'Close'}
</button>
<button
onClick={this.doSubmit}
type='submit'
className='btn btn-primary'
tabIndex='2'
>
{this.props.submitButtonText}
</button>
</Modal.Footer>
</form>
</Modal>
);
}
}
ChangeUrlModal.defaultProps = {
show: false,
title: 'Change URL',
desciption: '',
urlLabel: 'URL',
submitButtonText: 'Submit',
currentURL: '',
serverError: ''
};
ChangeUrlModal.propTypes = {
show: React.PropTypes.bool.isRequired,
title: React.PropTypes.string,
description: React.PropTypes.string,
urlLabel: React.PropTypes.string,
submitButtonText: React.PropTypes.string,
currentURL: React.PropTypes.string,
serverError: React.PropTypes.string,
onModalSubmit: React.PropTypes.func.isRequired,
onModalDismissed: React.PropTypes.func.isRequired
};

Просмотреть файл

@@ -20,12 +20,11 @@ export default class FileUpload extends React.Component {
}
fileUploadSuccess(channelId, data) {
var parsedData = $.parseJSON(data);
this.props.onFileUpload(parsedData.filenames, parsedData.client_ids, channelId);
this.props.onFileUpload(data.filenames, data.client_ids, channelId);
var requests = this.state.requests;
for (var j = 0; j < parsedData.client_ids.length; j++) {
delete requests[parsedData.client_ids[j]];
for (var j = 0; j < data.client_ids.length; j++) {
delete requests[data.client_ids[j]];
}
this.setState({requests: requests});
}
@@ -53,7 +52,7 @@ export default class FileUpload extends React.Component {
}
// generate a unique id that can be used by other components to refer back to this upload
var clientId = utils.generateId();
let clientId = utils.generateId();
// prepare data to be uploaded
var formData = new FormData();

Просмотреть файл

@@ -1,211 +0,0 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var utils = require('../utils/utils.jsx');
var client = require('../utils/client.jsx');
var asyncClient = require('../utils/async_client.jsx');
var UserStore = require('../stores/user_store.jsx');
export default class NewChannelModal extends React.Component {
constructor() {
super();
this.handleSubmit = this.handleSubmit.bind(this);
this.displayNameKeyUp = this.displayNameKeyUp.bind(this);
this.handleClose = this.handleClose.bind(this);
this.state = {channelType: ''};
}
handleSubmit(e) {
e.preventDefault();
var channel = {};
var state = {serverError: ''};
channel.display_name = React.findDOMNode(this.refs.display_name).value.trim();
if (!channel.display_name) {
state.displayNameError = 'This field is required';
state.inValid = true;
} else if (channel.display_name.length > 22) {
state.displayNameError = 'This field must be less than 22 characters';
state.inValid = true;
} else {
state.displayNameError = '';
}
channel.name = React.findDOMNode(this.refs.channel_name).value.trim();
if (!channel.name) {
state.nameError = 'This field is required';
state.inValid = true;
} else if (channel.name.length > 22) {
state.nameError = 'This field must be less than 22 characters';
state.inValid = true;
} else {
var cleanedName = utils.cleanUpUrlable(channel.name);
if (cleanedName !== channel.name) {
state.nameError = "Must be lowercase alphanumeric characters, allowing '-' but not starting or ending with '-'";
state.inValid = true;
} else {
state.nameError = '';
}
}
this.setState(state);
if (state.inValid) {
return;
}
var cu = UserStore.getCurrentUser();
channel.team_id = cu.team_id;
channel.description = React.findDOMNode(this.refs.channel_desc).value.trim();
channel.type = this.state.channelType;
client.createChannel(channel,
function success(data) {
$(React.findDOMNode(this.refs.modal)).modal('hide');
asyncClient.getChannel(data.id);
utils.switchChannel(data);
React.findDOMNode(this.refs.display_name).value = '';
React.findDOMNode(this.refs.channel_name).value = '';
React.findDOMNode(this.refs.channel_desc).value = '';
}.bind(this),
function error(err) {
state.serverError = err.message;
state.inValid = true;
this.setState(state);
}.bind(this)
);
}
displayNameKeyUp() {
var displayName = React.findDOMNode(this.refs.display_name).value.trim();
var channelName = utils.cleanUpUrlable(displayName);
React.findDOMNode(this.refs.channel_name).value = channelName;
}
componentDidMount() {
var self = this;
$(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function onModalShow(e) {
var button = e.relatedTarget;
self.setState({channelType: $(button).attr('data-channeltype')});
});
$(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.handleClose);
}
componentWillUnmount() {
$(React.findDOMNode(this.refs.modal)).off('hidden.bs.modal', this.handleClose);
}
handleClose() {
$(React.findDOMNode(this)).find('.form-control').each(function clearForms() {
this.value = '';
});
this.setState({channelType: '', displayNameError: '', nameError: '', serverError: '', inValid: false});
}
render() {
var displayNameError = null;
var nameError = null;
var serverError = null;
var displayNameClass = 'form-group';
var nameClass = 'form-group';
if (this.state.displayNameError) {
displayNameError = <label className='control-label'>{this.state.displayNameError}</label>;
displayNameClass += ' has-error';
}
if (this.state.nameError) {
nameError = <label className='control-label'>{this.state.nameError}</label>;
nameClass += ' has-error';
}
if (this.state.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
var channelTerm = 'Channel';
if (this.state.channelType === 'P') {
channelTerm = 'Group';
}
return (
<div
className='modal fade'
id='new_channel'
ref='modal'
tabIndex='-1'
role='dialog'
aria-hidden='true'
>
<div className='modal-dialog'>
<div className='modal-content'>
<div className='modal-header'>
<button
type='button'
className='close'
data-dismiss='modal'
>
<span aria-hidden='true'>&times;</span>
<span className='sr-only'>Cancel</span>
</button>
<h4 className='modal-title'>New {channelTerm}</h4>
</div>
<form role='form'>
<div className='modal-body'>
<div className={displayNameClass}>
<label className='control-label'>Display Name</label>
<input
onKeyUp={this.displayNameKeyUp}
type='text'
ref='display_name'
className='form-control'
placeholder='Enter display name'
maxLength='22'
/>
{displayNameError}
</div>
<div className={nameClass}>
<label className='control-label'>Handle</label>
<input
type='text'
className='form-control'
ref='channel_name'
placeholder="lowercase alphanumeric's only"
maxLength='22'
/>
{nameError}
</div>
<div className='form-group'>
<label className='control-label'>Description</label>
<textarea
className='form-control no-resize'
ref='channel_desc'
rows='3'
placeholder='Description'
maxLength='1024'
/>
</div>
{serverError}
</div>
<div className='modal-footer'>
<button
type='button'
className='btn btn-default'
data-dismiss='modal'
>
Cancel
</button>
<button
onClick={this.handleSubmit}
type='submit'
className='btn btn-primary'
>
Create New {channelTerm}
</button>
</div>
</form>
</div>
</div>
</div>
);
}
}

206
web/react/components/new_channel_flow.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,206 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Utils = require('../utils/utils.jsx');
var AsyncClient = require('../utils/async_client.jsx');
var Client = require('../utils/client.jsx');
var UserStore = require('../stores/user_store.jsx');
var NewChannelModal = require('./new_channel_modal.jsx');
var ChangeURLModal = require('./change_url_modal.jsx');
const SHOW_NEW_CHANNEL = 1;
const SHOW_EDIT_URL = 2;
const SHOW_EDIT_URL_THEN_COMPLETE = 3;
export default class NewChannelFlow extends React.Component {
constructor(props) {
super(props);
this.doSubmit = this.doSubmit.bind(this);
this.typeSwitched = this.typeSwitched.bind(this);
this.urlChangeRequested = this.urlChangeRequested.bind(this);
this.urlChangeSubmitted = this.urlChangeSubmitted.bind(this);
this.urlChangeDismissed = this.urlChangeDismissed.bind(this);
this.channelDataChanged = this.channelDataChanged.bind(this);
this.state = {
serverError: '',
channelType: 'O',
flowState: SHOW_NEW_CHANNEL,
channelDisplayName: '',
channelName: '',
channelDescription: '',
nameModified: false
};
}
componentWillReceiveProps(nextProps) {
// If we are being shown, grab channel type from props and clear
if (nextProps.show === true && this.props.show === false) {
this.setState({
serverError: '',
channelType: nextProps.channelType,
flowState: SHOW_NEW_CHANNEL,
channelDisplayName: '',
channelName: '',
channelDescription: '',
nameModified: false
});
}
}
doSubmit() {
var channel = {};
channel.display_name = this.state.channelDisplayName;
if (!channel.display_name) {
this.setState({serverError: 'Invalid Channel Name'});
return;
}
channel.name = this.state.channelName;
if (channel.name.length < 2) {
this.setState({flowState: SHOW_EDIT_URL_THEN_COMPLETE});
return;
}
const cu = UserStore.getCurrentUser();
channel.team_id = cu.team_id;
channel.description = this.state.channelDescription;
channel.type = this.state.channelType;
Client.createChannel(channel,
(data) => {
this.props.onModalDismissed();
AsyncClient.getChannel(data.id);
Utils.switchChannel(data);
},
(err) => {
if (err.message === 'Name must be 2 or more lowercase alphanumeric characters') {
this.setState({flowState: SHOW_EDIT_URL_THEN_COMPLETE});
}
if (err.message === 'A channel with that handle already exists') {
this.setState({serverError: 'A channel with that URL already exists'});
return;
}
this.setState({serverError: err.message});
}
);
}
typeSwitched() {
if (this.state.channelType === 'P') {
this.setState({channelType: 'O'});
} else {
this.setState({channelType: 'P'});
}
}
urlChangeRequested() {
this.setState({flowState: SHOW_EDIT_URL});
}
urlChangeSubmitted(newURL) {
if (this.state.flowState === SHOW_EDIT_URL_THEN_COMPLETE) {
this.setState({channelName: newURL, nameModified: true}, this.doSubmit);
} else {
this.setState({flowState: SHOW_NEW_CHANNEL, serverError: '', channelName: newURL, nameModified: true});
}
}
urlChangeDismissed() {
this.setState({flowState: SHOW_NEW_CHANNEL});
}
channelDataChanged(data) {
this.setState({
channelDisplayName: data.displayName,
channelDescription: data.description
});
if (!this.state.nameModified) {
this.setState({channelName: Utils.cleanUpUrlable(data.displayName.trim())});
}
}
render() {
const channelData = {
name: this.state.channelName,
displayName: this.state.channelDisplayName,
description: this.state.channelDescription
};
let showChannelModal = false;
let showGroupModal = false;
let showChangeURLModal = false;
let changeURLTitle = '';
let changeURLSubmitButtonText = '';
let channelTerm = '';
// Only listen to flow state if we are being shown
if (this.props.show) {
switch (this.state.flowState) {
case SHOW_NEW_CHANNEL:
if (this.state.channelType === 'O') {
showChannelModal = true;
channelTerm = 'Channel';
} else {
showGroupModal = true;
channelTerm = 'Group';
}
break;
case SHOW_EDIT_URL:
showChangeURLModal = true;
changeURLTitle = 'Change ' + channelTerm + ' URL';
changeURLSubmitButtonText = 'Change ' + channelTerm + ' URL';
break;
case SHOW_EDIT_URL_THEN_COMPLETE:
showChangeURLModal = true;
changeURLTitle = 'Set ' + channelTerm + ' URL';
changeURLSubmitButtonText = 'Create ' + channelTerm;
break;
}
}
return (
<span>
<NewChannelModal
show={showChannelModal}
channelType={'O'}
channelData={channelData}
serverError={this.state.serverError}
onSubmitChannel={this.doSubmit}
onModalDismissed={this.props.onModalDismissed}
onTypeSwitched={this.typeSwitched}
onChangeURLPressed={this.urlChangeRequested}
onDataChanged={this.channelDataChanged}
/>
<NewChannelModal
show={showGroupModal}
channelType={'P'}
channelData={channelData}
serverError={this.state.serverError}
onSubmitChannel={this.doSubmit}
onModalDismissed={this.props.onModalDismissed}
onTypeSwitched={this.typeSwitched}
onChangeURLPressed={this.urlChangeRequested}
onDataChanged={this.channelDataChanged}
/>
<ChangeURLModal
show={showChangeURLModal}
title={changeURLTitle}
description={'Some characters are not allowed in URLs and may be removed.'}
urlLabel={channelTerm + ' URL'}
submitButtonText={changeURLSubmitButtonText}
currentURL={this.state.channelName}
serverError={this.state.serverError}
onModalSubmit={this.urlChangeSubmitted}
onModalDismissed={this.urlChangeDismissed}
/>
</span>
);
}
}
NewChannelFlow.defaultProps = {
show: false,
channelType: 'O'
};
NewChannelFlow.propTypes = {
show: React.PropTypes.bool.isRequired,
channelType: React.PropTypes.string.isRequired,
onModalDismissed: React.PropTypes.func.isRequired
};

198
web/react/components/new_channel_modal.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,198 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
const Utils = require('../utils/utils.jsx');
var Modal = ReactBootstrap.Modal;
export default class NewChannelModal extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
this.state = {
displayNameError: ''
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.show === true && this.props.show === false) {
this.setState({
displayNameError: ''
});
}
}
handleSubmit(e) {
e.preventDefault();
const displayName = React.findDOMNode(this.refs.display_name).value.trim();
if (displayName.length < 1) {
this.setState({displayNameError: 'This field is required'});
return;
}
this.props.onSubmitChannel();
}
handleChange() {
const newData = {
displayName: React.findDOMNode(this.refs.display_name).value,
description: React.findDOMNode(this.refs.channel_desc).value
};
this.props.onDataChanged(newData);
}
render() {
var displayNameError = null;
var serverError = null;
var displayNameClass = 'form-group';
if (this.state.displayNameError) {
displayNameError = <p className='input__help error'>{this.state.displayNameError}</p>;
displayNameClass += ' has-error';
}
if (this.props.serverError) {
serverError = <div className='form-group has-error'><p className='input__help error'>{this.props.serverError}</p></div>;
}
var channelTerm = '';
var channelSwitchText = '';
switch (this.props.channelType) {
case 'P':
channelTerm = 'Group';
channelSwitchText = (
<div className='modal-intro'>
{'Create a new private group with restricted membership. '}
<a
href='#'
onClick={this.props.onTypeSwitched}
>
{'Create a public channel'}
</a>
</div>
);
break;
case 'O':
channelTerm = 'Channel';
channelSwitchText = (
<div className='modal-intro'>
{'Create a new public channel anyone can join. '}
<a
href='#'
onClick={this.props.onTypeSwitched}
>
{'Create a private group'}
</a>
</div>
);
break;
}
const prettyTeamURL = Utils.getShortenedTeamURL();
return (
<span>
<Modal
show={this.props.show}
onHide={this.props.onModalDismissed}
>
<Modal.Header closeButton={true}>
<Modal.Title>{'New ' + channelTerm}</Modal.Title>
</Modal.Header>
<form
role='form'
className='form-horizontal'
>
<Modal.Body>
<div>
{channelSwitchText}
</div>
<div className={displayNameClass}>
<label className='col-sm-2 form__label control-label'>{'Name'}</label>
<div className='col-sm-10'>
<input
onChange={this.handleChange}
type='text'
ref='display_name'
className='form-control'
placeholder='Ex: "Bugs", "Marketing", "办公室恋情"'
maxLength='22'
value={this.props.channelData.displayName}
autoFocus={true}
tabIndex='1'
/>
{displayNameError}
<p className='input__help'>
{'Channel URL: ' + prettyTeamURL + this.props.channelData.name + ' ('}
<a
href='#'
onClick={this.props.onChangeURLPressed}
>
{'change this URL'}
</a>
{')'}
</p>
</div>
</div>
<div className='form-group less'>
<div className='col-sm-2'>
<label className='form__label control-label'>{'Description'}</label>
<label className='form__label light'>{'(optional)'}</label>
</div>
<div className='col-sm-10'>
<textarea
className='form-control no-resize'
ref='channel_desc'
rows='4'
placeholder='Description'
maxLength='1024'
value={this.props.channelData.description}
onChange={this.handleChange}
tabIndex='2'
/>
<p className='input__help'>
{'The purpose of your channel. To help others decide whether to join.'}
</p>
{serverError}
</div>
</div>
</Modal.Body>
<Modal.Footer>
<button
type='button'
className='btn btn-default'
onClick={this.props.onModalDismissed}
>
{'Cancel'}
</button>
<button
onClick={this.handleSubmit}
type='submit'
className='btn btn-primary'
tabIndex='3'
>
{'Create New ' + channelTerm}
</button>
</Modal.Footer>
</form>
</Modal>
</span>
);
}
}
NewChannelModal.defaultProps = {
show: false,
channelType: 'O',
serverError: ''
};
NewChannelModal.propTypes = {
show: React.PropTypes.bool.isRequired,
channelType: React.PropTypes.string.isRequired,
channelData: React.PropTypes.object.isRequired,
serverError: React.PropTypes.string,
onSubmitChannel: React.PropTypes.func.isRequired,
onModalDismissed: React.PropTypes.func.isRequired,
onTypeSwitched: React.PropTypes.func.isRequired,
onChangeURLPressed: React.PropTypes.func.isRequired,
onDataChanged: React.PropTypes.func.isRequired
};

Просмотреть файл

@@ -152,7 +152,7 @@ export default class Post extends React.Component {
return (
<div>
<div
id={post.id}
id={'post_' + post.id}
className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss}
>
{profilePic}

Просмотреть файл

@@ -189,9 +189,15 @@ export default class PostList extends React.Component {
this.scrollToBottom(true);
// the user clicked 'load more messages'
} else if (this.gotMorePosts) {
var lastPost = oldPosts[oldOrder[prevState.numToDisplay]];
$('#' + lastPost.id)[0].scrollIntoView();
} else if (this.gotMorePosts && oldOrder.length > 0) {
let index;
if (prevState.numToDisplay >= oldOrder.length) {
index = oldOrder.length - 1;
} else {
index = prevState.numToDisplay;
}
const lastPost = oldPosts[oldOrder[index]];
$('#post_' + lastPost.id)[0].scrollIntoView();
this.gotMorePosts = false;
} else {
this.scrollTo(this.prevScrollTop);

Просмотреть файл

@@ -12,6 +12,7 @@ var Utils = require('../utils/utils.jsx');
var SidebarHeader = require('./sidebar_header.jsx');
var SearchBox = require('./search_bar.jsx');
var Constants = require('../utils/constants.jsx');
var NewChannelFlow = require('./new_channel_flow.jsx');
export default class Sidebar extends React.Component {
constructor(props) {
@@ -28,6 +29,7 @@ export default class Sidebar extends React.Component {
this.createChannelElement = this.createChannelElement.bind(this);
this.state = this.getStateFromStores();
this.state.modal = '';
this.state.loadingDMChannel = -1;
}
getStateFromStores() {
@@ -473,8 +475,18 @@ export default class Sidebar extends React.Component {
);
}
let showChannelModal = false;
if (this.state.modal !== '') {
showChannelModal = true;
}
return (
<div>
<NewChannelFlow
show={showChannelModal}
channelType={this.state.modal}
onModalDismissed={() => this.setState({modal: ''})}
/>
<SidebarHeader
teamDisplayName={this.props.teamDisplayName}
teamType={this.props.teamType}
@@ -508,11 +520,9 @@ export default class Sidebar extends React.Component {
<a
className='add-channel-btn'
href='#'
data-toggle='modal'
data-target='#new_channel'
data-channeltype='O'
onClick={() => this.setState({modal: 'O'})}
>
+
{'+'}
</a>
</h4>
</li>
@@ -537,11 +547,9 @@ export default class Sidebar extends React.Component {
<a
className='add-channel-btn'
href='#'
data-toggle='modal'
data-target='#new_channel'
data-channeltype='P'
onClick={() => this.setState({modal: 'P'})}
>
+
{'+'}
</a>
</h4>
</li>

Просмотреть файл

@@ -32,7 +32,7 @@ export default class SignupUserComplete extends React.Component {
handleSubmit(e) {
e.preventDefault();
this.state.user.username = React.findDOMNode(this.refs.name).value.trim();
this.state.user.username = React.findDOMNode(this.refs.name).value.trim().toLowerCase();
if (!this.state.user.username) {
this.setState({nameError: 'This field is required', emailError: '', passwordError: '', serverError: ''});
return;

Просмотреть файл

@@ -35,7 +35,7 @@ export default class TeamImportTab extends React.Component {
var uploadHelpText = (
<div>
<p>{'Slack does not allow you to export files, images, private groups or direct messages stored in Slack. Therefore, Slack import to Mattermost only supports importing of text messages in your Slack team\'\s public channels.'}</p>
<p>{'The Slack import to Mattermost is in "Preview". Slack bot posts and channels with underscores do not yet import.'}</p>
<p>{'The Slack import to Mattermost is in "Preview". Slack bot posts do not yet import.'}</p>
</div>
);

Просмотреть файл

@@ -22,7 +22,7 @@ export default class TeamSignupUsernamePage extends React.Component {
submitNext(e) {
e.preventDefault();
var name = React.findDOMNode(this.refs.name).value.trim();
var name = React.findDOMNode(this.refs.name).value.trim().toLowerCase();
var usernameError = Utils.isValidUsername(name);
if (usernameError === 'Cannot use a reserved word as a username.') {

Просмотреть файл

@@ -40,7 +40,7 @@ export default class UserSettingsGeneralTab extends React.Component {
e.preventDefault();
var user = this.props.user;
var username = this.state.username.trim();
var username = this.state.username.trim().toLowerCase();
var usernameError = utils.isValidUsername(username);
if (usernameError === 'Cannot use a reserved word as a username.') {

Просмотреть файл

@@ -105,6 +105,14 @@ export default class ViewImageModal extends React.Component {
this.loadImage(this.state.imgId);
}.bind(this));
$('#' + this.props.modalId).on('hidden.bs.modal', function onModalHide() {
if (this.refs.video) {
var video = React.findDOMNode(this.refs.video);
video.pause();
video.currentTime = 0;
}
}.bind(this));
$(React.findDOMNode(this.refs.modal)).click(function onModalClick(e) {
if (e.target === this || e.target === React.findDOMNode(this.refs.imageBody)) {
$('.image_modal').modal('hide');
@@ -211,6 +219,16 @@ export default class ViewImageModal extends React.Component {
/>
</a>
);
} else if (fileType === 'video' || fileType === 'audio') {
content = (
<video
ref='video'
data-setup='{}'
controls='controls'
>
<source src={Utils.getWindowLocationOrigin() + '/api/v1/files/get' + filename} />
</video>
);
} else {
// non-image files include a section providing details about the file
var infoString = 'File type ' + fileInfo.ext.toUpperCase();

Просмотреть файл

@@ -3,22 +3,21 @@
"version": "0.0.1",
"private": true,
"dependencies": {
"autolinker": "^0.18.1",
"flux": "^2.1.1",
"keymirror": "^0.1.1",
"object-assign": "^3.0.0",
"react": "^0.13.3",
"react-zeroclipboard-mixin": "^0.1.0",
"twemoji": "^1.4.1"
"autolinker": "0.18.1",
"flux": "2.1.1",
"keymirror": "0.1.1",
"object-assign": "3.0.0",
"react-zeroclipboard-mixin": "0.1.0",
"twemoji": "1.4.1"
},
"devDependencies": {
"browserify": "^11.0.1",
"envify": "^3.4.0",
"babelify": "^6.1.3",
"uglify-js": "^2.4.24",
"watchify": "^3.3.1",
"eslint": "^1.3.1",
"eslint-plugin-react": "^3.3.1"
"browserify": "11.0.1",
"envify": "3.4.0",
"babelify": "6.1.3",
"uglify-js": "2.4.24",
"watchify": "3.3.1",
"eslint": "1.3.1",
"eslint-plugin-react": "3.3.1"
},
"scripts": {
"start": "watchify --extension=jsx -o ../static/js/bundle.js -v -d ./**/*.jsx",

Просмотреть файл

@@ -17,7 +17,6 @@ var RenameChannelModal = require('../components/rename_channel_modal.jsx');
var EditPostModal = require('../components/edit_post_modal.jsx');
var DeletePostModal = require('../components/delete_post_modal.jsx');
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/user_settings_modal.jsx');
@@ -153,11 +152,6 @@ function setupChannelPage(teamName, teamType, teamId, channelName, channelId) {
document.getElementById('direct_channel_modal')
);
React.render(
<NewChannelModal />,
document.getElementById('new_channel_modal')
);
React.render(
<PostListContainer />,
document.getElementById('post-list')

Просмотреть файл

@@ -828,14 +828,12 @@ export function isValidUsername(name) {
} else if (name.length < 3 || name.length > 15) {
error = 'Must be between 3 and 15 characters';
} else if (!(/^[a-z0-9\.\-\_]+$/).test(name)) {
error = "Must contain only lowercase letters, numbers, and the symbols '.', '-', and '_'.";
error = "Must contain only letters, numbers, and the symbols '.', '-', and '_'.";
} else if (!(/[a-z]/).test(name.charAt(0))) {
error = 'First character must be a letter.';
} else {
var lowerName = name.toLowerCase().trim();
for (var i = 0; i < Constants.RESERVED_USERNAMES.length; i++) {
if (lowerName === Constants.RESERVED_USERNAMES[i]) {
if (name === Constants.RESERVED_USERNAMES[i]) {
error = 'Cannot use a reserved word as a username.';
break;
}
@@ -1127,3 +1125,14 @@ export function importSlack(file, success, error) {
client.importSlack(formData, success, error);
}
export function getTeamURLFromAddressBar() {
return window.location.href.split('/channels')[0];
}
export function getShortenedTeamURL() {
const teamURL = getTeamURLFromAddressBar();
if (teamURL.length > 24) {
return teamURL.substring(0, 10) + '...' + teamURL.substring(teamURL.length - 12, teamURL.length - 1) + '/';
}
}

33
web/sass-files/sass/partials/_forms.scss Обычный файл
Просмотреть файл

@@ -0,0 +1,33 @@
.form-horizontal {
.modal-intro {
margin: -10px 0 30px;
}
.form__label {
text-align: left;
padding-right: 3px;
font-weight: bold;
font-size: 1.1em;
&.light {
color: #999;
font-size: 1.05em;
font-style: italic;
padding-top: 2px;
}
}
.input__help {
color: #777;
margin: 10px 0 0 10px;
&.error {
color: #a94442;
}
}
.form-control {
font-weight: normal;
}
.form-group {
margin-bottom: 25px;
&.less {
margin-bottom: 10px;
}
}
}

Просмотреть файл

@@ -145,6 +145,8 @@
li a {
padding: 3px 20px;
color: #555;
text-overflow: ellipsis;
overflow: hidden;
}
}
.dropdown__icon {

Просмотреть файл

@@ -123,6 +123,7 @@
}
.more-channel-table {
margin: 0;
table-layout: fixed;
p {
font-size: 0.9em;
overflow: hidden;
@@ -154,7 +155,7 @@
&.td--action {
text-align: right;
padding: 8px 15px 8px 8px;
width: 70px;
width: 80px;
vertical-align: middle;
}
}

Просмотреть файл

@@ -12,7 +12,10 @@
}
.dropdown-menu {
max-height: 300px;
overflow: auto;
overflow-x: hidden;
overflow-y: auto;
max-width: 200px;
width: 200px;
}
.search__form {
margin: 0;
@@ -49,7 +52,7 @@
left: 0;
right: 0;
width: 72%;
color: #777;
color: #fff;
background: #2389D7;
@include border-radius(50px);
margin: 0 auto;

Просмотреть файл

@@ -29,6 +29,7 @@
@import "partials/videos";
@import "partials/settings";
@import "partials/modal";
@import "partials/forms";
@import "partials/mentions";
@import "partials/command-box";
@import "partials/error";

5
web/static/css/bootstrap-3.3.1.min.css поставляемый

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Разница между файлами не показана из-за своего большого размера Загрузить разницу

5
web/static/css/bootstrap-3.3.5.min.css поставляемый Обычный файл

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

7
web/static/js/bootstrap-3.3.1.min.js поставляемый

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Разница между файлами не показана из-за своего большого размера Загрузить разницу

7
web/static/js/bootstrap-3.3.5.min.js поставляемый Обычный файл

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

1024
web/static/js/jasny-bootstrap.js Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

6
web/static/js/jasny-bootstrap.min.js поставляемый Обычный файл

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

15678
web/static/js/react-bootstrap-0.25.1.js Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

14
web/static/js/react-bootstrap-0.25.1.min.js поставляемый Обычный файл

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

16
web/static/js/react-with-addons-0.13.1.min.js поставляемый

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@@ -1,5 +1,5 @@
/**
* React (with addons) v0.13.1
* React (with addons) v0.13.3
*/
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.React = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
/**
@@ -710,7 +710,9 @@ var isUnitlessNumber = {
columnCount: true,
flex: true,
flexGrow: true,
flexPositive: true,
flexShrink: true,
flexNegative: true,
fontWeight: true,
lineClamp: true,
lineHeight: true,
@@ -723,7 +725,9 @@ var isUnitlessNumber = {
// SVG-related properties
fillOpacity: true,
strokeOpacity: true
strokeDashoffset: true,
strokeOpacity: true,
strokeWidth: true
};
/**
@@ -3788,6 +3792,7 @@ var HTMLDOMPropertyConfig = {
headers: null,
height: MUST_USE_ATTRIBUTE,
hidden: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
high: null,
href: null,
hrefLang: null,
htmlFor: null,
@@ -3798,6 +3803,7 @@ var HTMLDOMPropertyConfig = {
lang: null,
list: MUST_USE_ATTRIBUTE,
loop: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
low: null,
manifest: MUST_USE_ATTRIBUTE,
marginHeight: null,
marginWidth: null,
@@ -3812,6 +3818,7 @@ var HTMLDOMPropertyConfig = {
name: null,
noValidate: HAS_BOOLEAN_VALUE,
open: HAS_BOOLEAN_VALUE,
optimum: null,
pattern: null,
placeholder: null,
poster: null,
@@ -3825,6 +3832,7 @@ var HTMLDOMPropertyConfig = {
rowSpan: null,
sandbox: null,
scope: null,
scoped: HAS_BOOLEAN_VALUE,
scrolling: null,
seamless: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
selected: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
@@ -3866,7 +3874,9 @@ var HTMLDOMPropertyConfig = {
itemID: MUST_USE_ATTRIBUTE,
itemRef: MUST_USE_ATTRIBUTE,
// property is supported for OpenGraph in meta tags.
property: null
property: null,
// IE-only attribute that controls focus behavior
unselectable: MUST_USE_ATTRIBUTE
},
DOMAttributeNames: {
acceptCharset: 'accept-charset',
@@ -4475,7 +4485,7 @@ if ("production" !== "development") {
if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') {
console.debug(
'Download the React DevTools for a better development experience: ' +
'http://fb.me/react-devtools'
'https://fb.me/react-devtools'
);
}
}
@@ -4502,7 +4512,7 @@ if ("production" !== "development") {
if (!expectedFeatures[i]) {
console.error(
'One or more ES5 shim/shams expected by React are not available: ' +
'http://fb.me/react-warning-polyfills'
'https://fb.me/react-warning-polyfills'
);
break;
}
@@ -4510,7 +4520,7 @@ if ("production" !== "development") {
}
}
React.version = '0.13.1';
React.version = '0.13.3';
module.exports = React;
@@ -6229,7 +6239,7 @@ var ReactClass = {
("production" !== "development" ? warning(
this instanceof Constructor,
'Something is calling a React component directly. Use a factory or ' +
'JSX instead. See: http://fb.me/react-legacyfactory'
'JSX instead. See: https://fb.me/react-legacyfactory'
) : null);
}
@@ -6439,20 +6449,38 @@ ReactComponent.prototype.forceUpdate = function(callback) {
*/
if ("production" !== "development") {
var deprecatedAPIs = {
getDOMNode: 'getDOMNode',
isMounted: 'isMounted',
replaceProps: 'replaceProps',
replaceState: 'replaceState',
setProps: 'setProps'
getDOMNode: [
'getDOMNode',
'Use React.findDOMNode(component) instead.'
],
isMounted: [
'isMounted',
'Instead, make sure to clean up subscriptions and pending requests in ' +
'componentWillUnmount to prevent memory leaks.'
],
replaceProps: [
'replaceProps',
'Instead call React.render again at the top level.'
],
replaceState: [
'replaceState',
'Refactor your code to use setState instead (see ' +
'https://github.com/facebook/react/issues/3236).'
],
setProps: [
'setProps',
'Instead call React.render again at the top level.'
]
};
var defineDeprecationWarning = function(methodName, displayName) {
var defineDeprecationWarning = function(methodName, info) {
try {
Object.defineProperty(ReactComponent.prototype, methodName, {
get: function() {
("production" !== "development" ? warning(
false,
'%s(...) is deprecated in plain JavaScript React classes.',
displayName
'%s(...) is deprecated in plain JavaScript React classes. %s',
info[0],
info[1]
) : null);
return undefined;
}
@@ -6802,6 +6830,14 @@ var ReactCompositeComponentMixin = {
'Did you mean to define a state property instead?',
this.getName() || 'a component'
) : null);
("production" !== "development" ? warning(
!inst.getDefaultProps ||
inst.getDefaultProps.isReactClassApproved,
'getDefaultProps was defined on %s, a plain JavaScript class. ' +
'This is only supported for classes created using React.createClass. ' +
'Use a static property to define defaultProps instead.',
this.getName() || 'a component'
) : null);
("production" !== "development" ? warning(
!inst.propTypes,
'propTypes was defined as an instance property on %s. Use a static ' +
@@ -6838,6 +6874,7 @@ var ReactCompositeComponentMixin = {
this._pendingReplaceState = false;
this._pendingForceUpdate = false;
var childContext;
var renderedElement;
var previouslyMounting = ReactLifeCycle.currentlyMountingInstance;
@@ -6852,7 +6889,8 @@ var ReactCompositeComponentMixin = {
}
}
renderedElement = this._renderValidatedComponent();
childContext = this._getValidatedChildContext(context);
renderedElement = this._renderValidatedComponent(childContext);
} finally {
ReactLifeCycle.currentlyMountingInstance = previouslyMounting;
}
@@ -6866,7 +6904,7 @@ var ReactCompositeComponentMixin = {
this._renderedComponent,
rootID,
transaction,
this._processChildContext(context)
this._mergeChildContext(context, childContext)
);
if (inst.componentDidMount) {
transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
@@ -6996,7 +7034,7 @@ var ReactCompositeComponentMixin = {
* @return {object}
* @private
*/
_processChildContext: function(currentContext) {
_getValidatedChildContext: function(currentContext) {
var inst = this._instance;
var childContext = inst.getChildContext && inst.getChildContext();
if (childContext) {
@@ -7021,6 +7059,13 @@ var ReactCompositeComponentMixin = {
name
) : invariant(name in inst.constructor.childContextTypes));
}
return childContext;
}
return null;
},
_mergeChildContext: function(currentContext, childContext) {
if (childContext) {
return assign({}, currentContext, childContext);
}
return currentContext;
@@ -7280,6 +7325,10 @@ var ReactCompositeComponentMixin = {
return inst.state;
}
if (replace && queue.length === 1) {
return queue[0];
}
var nextState = assign({}, replace ? queue[0] : inst.state);
for (var i = replace ? 1 : 0; i < queue.length; i++) {
var partial = queue[i];
@@ -7349,13 +7398,14 @@ var ReactCompositeComponentMixin = {
_updateRenderedComponent: function(transaction, context) {
var prevComponentInstance = this._renderedComponent;
var prevRenderedElement = prevComponentInstance._currentElement;
var nextRenderedElement = this._renderValidatedComponent();
var childContext = this._getValidatedChildContext();
var nextRenderedElement = this._renderValidatedComponent(childContext);
if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
ReactReconciler.receiveComponent(
prevComponentInstance,
nextRenderedElement,
transaction,
this._processChildContext(context)
this._mergeChildContext(context, childContext)
);
} else {
// These two IDs are actually the same! But nothing should rely on that.
@@ -7371,7 +7421,7 @@ var ReactCompositeComponentMixin = {
this._renderedComponent,
thisID,
transaction,
context
this._mergeChildContext(context, childContext)
);
this._replaceNodeWithMarkupByID(prevComponentID, nextMarkup);
}
@@ -7409,11 +7459,12 @@ var ReactCompositeComponentMixin = {
/**
* @private
*/
_renderValidatedComponent: function() {
_renderValidatedComponent: function(childContext) {
var renderedComponent;
var previousContext = ReactContext.current;
ReactContext.current = this._processChildContext(
this._currentElement._context
ReactContext.current = this._mergeChildContext(
this._currentElement._context,
childContext
);
ReactCurrentOwner.current = this;
try {
@@ -7778,6 +7829,7 @@ var ReactDOM = mapObject({
// SVG
circle: 'circle',
clipPath: 'clipPath',
defs: 'defs',
ellipse: 'ellipse',
g: 'g',
@@ -7927,11 +7979,13 @@ function assertValidProps(props) {
'Can only set one of `children` or `props.dangerouslySetInnerHTML`.'
) : invariant(props.children == null));
("production" !== "development" ? invariant(
props.dangerouslySetInnerHTML.__html != null,
typeof props.dangerouslySetInnerHTML === 'object' &&
'__html' in props.dangerouslySetInnerHTML,
'`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
'Please visit http://fb.me/react-invariant-dangerously-set-inner-html ' +
'Please visit https://fb.me/react-invariant-dangerously-set-inner-html ' +
'for more information.'
) : invariant(props.dangerouslySetInnerHTML.__html != null));
) : invariant(typeof props.dangerouslySetInnerHTML === 'object' &&
'__html' in props.dangerouslySetInnerHTML));
}
if ("production" !== "development") {
("production" !== "development" ? warning(
@@ -8239,6 +8293,8 @@ ReactDOMComponent.Mixin = {
if (propKey === STYLE) {
if (nextProp) {
nextProp = this._previousStyleCopy = assign({}, nextProp);
} else {
this._previousStyleCopy = null;
}
if (lastProp) {
// Unset styles on `lastProp` but not on `nextProp`.
@@ -10721,7 +10777,7 @@ function warnAndMonitorForKeyUse(message, element, parentType) {
("production" !== "development" ? warning(
false,
message + '%s%s See http://fb.me/react-warning-keys for more information.',
message + '%s%s See https://fb.me/react-warning-keys for more information.',
parentOrOwnerAddendum,
childOwnerAddendum
) : null);
@@ -10845,9 +10901,9 @@ function warnForPropsMutation(propName, element) {
("production" !== "development" ? warning(
false,
'Don\'t set .props.%s of the React component%s. ' +
'Instead, specify the correct value when ' +
'initially creating the element.%s',
'Don\'t set .props.%s of the React component%s. Instead, specify the ' +
'correct value when initially creating the element or use ' +
'React.cloneElement to make a new element with updated props.%s',
propName,
elementInfo,
ownerInfo
@@ -15236,6 +15292,7 @@ var ReactUpdates = _dereq_(100);
var SyntheticEvent = _dereq_(108);
var assign = _dereq_(29);
var emptyObject = _dereq_(130);
var topLevelTypes = EventConstants.topLevelTypes;
@@ -15577,6 +15634,9 @@ assign(
);
ReactShallowRenderer.prototype.render = function(element, context) {
if (!context) {
context = emptyObject;
}
var transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
this._render(element, transaction, context);
ReactUpdates.ReactReconcileTransaction.release(transaction);
@@ -15717,7 +15777,7 @@ for (eventType in topLevelTypes) {
module.exports = ReactTestUtils;
},{"100":100,"108":108,"16":16,"18":18,"21":21,"29":29,"31":31,"33":33,"43":43,"63":63,"65":65,"72":72,"73":73,"77":77}],96:[function(_dereq_,module,exports){
},{"100":100,"108":108,"130":130,"16":16,"18":18,"21":21,"29":29,"31":31,"33":33,"43":43,"63":63,"65":65,"72":72,"73":73,"77":77}],96:[function(_dereq_,module,exports){
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
@@ -16762,6 +16822,7 @@ var MUST_USE_ATTRIBUTE = DOMProperty.injection.MUST_USE_ATTRIBUTE;
var SVGDOMPropertyConfig = {
Properties: {
clipPath: MUST_USE_ATTRIBUTE,
cx: MUST_USE_ATTRIBUTE,
cy: MUST_USE_ATTRIBUTE,
d: MUST_USE_ATTRIBUTE,
@@ -16807,6 +16868,7 @@ var SVGDOMPropertyConfig = {
y: MUST_USE_ATTRIBUTE
},
DOMAttributeNames: {
clipPath: 'clip-path',
fillOpacity: 'fill-opacity',
fontFamily: 'font-family',
fontSize: 'font-size',
@@ -19713,6 +19775,7 @@ var shouldWrap = {
// Force wrapping for SVG elements because if they get created inside a <div>,
// they will be initialized in the wrong namespace (and will not display).
'circle': true,
'clipPath': true,
'defs': true,
'ellipse': true,
'g': true,
@@ -19755,6 +19818,7 @@ var markupWrap = {
'th': trWrap,
'circle': svgWrap,
'clipPath': svgWrap,
'defs': svgWrap,
'ellipse': svgWrap,
'g': svgWrap,
@@ -20100,6 +20164,7 @@ assign(
function isInternalComponentType(type) {
return (
typeof type === 'function' &&
typeof type.prototype !== 'undefined' &&
typeof type.prototype.mountComponent === 'function' &&
typeof type.prototype.receiveComponent === 'function'
);
@@ -21355,11 +21420,14 @@ module.exports = traverseAllChildren;
* @providesModule update
*/
/* global hasOwnProperty:true */
'use strict';
var assign = _dereq_(29);
var keyOf = _dereq_(157);
var invariant = _dereq_(150);
var hasOwnProperty = {}.hasOwnProperty;
function shallowCopy(x) {
if (Array.isArray(x)) {
@@ -21419,7 +21487,7 @@ function update(value, spec) {
COMMAND_SET
) : invariant(typeof spec === 'object'));
if (spec.hasOwnProperty(COMMAND_SET)) {
if (hasOwnProperty.call(spec, COMMAND_SET)) {
("production" !== "development" ? invariant(
Object.keys(spec).length === 1,
'Cannot have more than one key in an object with %s',
@@ -21431,7 +21499,7 @@ function update(value, spec) {
var nextValue = shallowCopy(value);
if (spec.hasOwnProperty(COMMAND_MERGE)) {
if (hasOwnProperty.call(spec, COMMAND_MERGE)) {
var mergeObj = spec[COMMAND_MERGE];
("production" !== "development" ? invariant(
mergeObj && typeof mergeObj === 'object',
@@ -21448,21 +21516,21 @@ function update(value, spec) {
assign(nextValue, spec[COMMAND_MERGE]);
}
if (spec.hasOwnProperty(COMMAND_PUSH)) {
if (hasOwnProperty.call(spec, COMMAND_PUSH)) {
invariantArrayCase(value, spec, COMMAND_PUSH);
spec[COMMAND_PUSH].forEach(function(item) {
nextValue.push(item);
});
}
if (spec.hasOwnProperty(COMMAND_UNSHIFT)) {
if (hasOwnProperty.call(spec, COMMAND_UNSHIFT)) {
invariantArrayCase(value, spec, COMMAND_UNSHIFT);
spec[COMMAND_UNSHIFT].forEach(function(item) {
nextValue.unshift(item);
});
}
if (spec.hasOwnProperty(COMMAND_SPLICE)) {
if (hasOwnProperty.call(spec, COMMAND_SPLICE)) {
("production" !== "development" ? invariant(
Array.isArray(value),
'Expected %s target to be an array; got %s',
@@ -21488,7 +21556,7 @@ function update(value, spec) {
});
}
if (spec.hasOwnProperty(COMMAND_APPLY)) {
if (hasOwnProperty.call(spec, COMMAND_APPLY)) {
("production" !== "development" ? invariant(
typeof spec[COMMAND_APPLY] === 'function',
'update(): expected spec of %s to be a function; got %s.',

18
web/static/js/react-with-addons-0.13.3.min.js поставляемый Обычный файл

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@@ -52,6 +52,7 @@
<script>
window.setup_channel_page('{{ .Props.TeamDisplayName }}', '{{ .Props.TeamType }}', '{{ .Props.TeamId }}', '{{ .Props.ChannelName }}', '{{ .Props.ChannelId }}');
$('body').tooltip( {selector: '[data-toggle=tooltip]'} );
$('.modal-body').css('max-height', $(window).height() * 0.7);
$('.modal-body').perfectScrollbar();
</script>
</body>

Просмотреть файл

@@ -18,12 +18,13 @@
<link rel="manifest" href="/static/config/manifest.json">
<!-- Android add to homescreen -->
<link rel="stylesheet" href="/static/css/bootstrap-3.3.1.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-3.3.5.min.css">
<link rel="stylesheet" href="/static/css/jasny-bootstrap.min.css" rel="stylesheet">
<script src="/static/js/react-with-addons-0.13.1.js"></script>
<script src="/static/js/react-with-addons-0.13.3.js"></script>
<script src="/static/js/jquery-1.11.1.js"></script>
<script src="/static/js/bootstrap-3.3.1.js"></script>
<script src="/static/js/bootstrap-3.3.5.js"></script>
<script src="/static/js/react-bootstrap-0.25.1.js"></script>
<link id="favicon" rel="icon" href="/static/images/favicon.ico" type="image/x-icon">
<link rel="shortcut icon" href="/static/images/favicon.ico" type="image/x-icon">

Просмотреть файл

@@ -36,9 +36,14 @@ func TearDown() {
func TestStatic(t *testing.T) {
Setup()
resp, _ := http.Get(URL + "/static/images/favicon.ico")
// add a short delay to make sure the server is ready to receive requests
time.Sleep(1 * time.Second)
if resp.StatusCode != http.StatusOK {
resp, err := http.Get(URL + "/static/images/favicon.ico")
if err != nil {
t.Fatalf("got error while trying to get static files %v", err)
} else if resp.StatusCode != http.StatusOK {
t.Fatalf("couldn't get static files %v", resp.StatusCode)
}
}