Fixing merge conflict

This commit is contained in:
=Corey Hulen
2015-10-22 09:19:18 -07:00
31 changed files with 578 additions and 120 deletions

View File

@@ -145,7 +145,6 @@ Messaging, Comments and Notifications
- Full markdown support in messages, comments, and channel description
- Support for emoji codes rendering to image files
Files and Images
- Added ability to play video and audio files
@@ -198,86 +197,86 @@ Licensing
### Bug Fixes
- Fixed issue so that SSO option automatically set EmailVerified=true (it was false previously)
- Fixed issue so that SSO option automatically set `EmailVerified=true` (it was false previously)
### Compatibility
A large number of settings were changed in [`config.json`](./config/config.json) and a System Console UI was added. This is a very large change due to Mattermost releasing as v1.0 and it's unlikely a change of this size would happen again.
Prior to upgrading the Mattermost binaries from the previous versions, the below options would need to be manually updated in existing config.json file to migrate successfully. This is a list of changes and their new default values in a fresh install:
Prior to upgrading the Mattermost binaries from the previous versions, the below options would need to be manually updated in your existing config.json file to migrate successfully. This is a list of changes and their new default values in a fresh install:
#### Config.json Changes from v0.7 to v1.0
##### Service Settings
- Under `ServiceSettings` in [`config.json`](./config/config.json):
- **Moved:** `"SiteName": "Mattermost"` which was added to `TeamSettings`
- **Removed:** `"Mode" : "dev"` which deprecates a high level dev mode, now replaced by granular controls
- **Renamed:** `"AllowTesting" : false` to `"EnableTesting": false` which allows the use of `/loadtest` slash commands during development
- **Removed:** `"UseSSL": false` boolean replaced by `"ConnectionSecurity": ""` under `Security` with new options: _None_ (`""`), _TLS_ (`"TLS"`) and _StartTLS_ ('"StartTLS"`)
- **Renamed**: `"Port": "8065"` to `"ListenAddress": ":8065"` to define address on which to listen. Must be prepended with a colon.
- **Removed:** `"Version": "developer"` removed and version information now stored in `model/version.go`
- **Removed:** `"Shards": {}` which was not used
- **Moved:** `"InviteSalt": "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"` to `EmailSettings`
- **Moved:** `"PublicLinkSalt": "TO3pTyXIZzwHiwyZgGql7lM7DG3zeId4"` to `FileSettings`
- **Renamed and Moved** `"ResetSalt": "IPxFzSfnDFsNsRafZxz8NaYqFKhf9y2t"` to `"PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL"` and moved to `EmailSettings`
- **Removed:** `"AnalyticsUrl": ""` which was not used
- **Removed:** `"UseLocalStorage": true` which is replaced by `"DriverName": "local"` in `FileSettings`
- **Renamed and Moved:** `"StorageDirectory": "./data/"` to `Directory` and moved to `FileSettings`
- **Renamed:** `"AllowedLoginAttempts": 10` to `"MaximumLoginAttempts": 10`
- **Renamed, Reversed and Moved:** `"DisableEmailSignUp": false` renamed `"EnableSignUpWithEmail": true`, reversed meaning of `true`, and moved to `EmailSettings`
- **Added:** `"EnableOAuthServiceProvider": false` to enable OAuth2 service provider functionality
- **Added:** `"EnableIncomingWebhooks": false` to enable incoming webhooks feature
- Moved: `"SiteName": "Mattermost"` which was added to `TeamSettings`
- Removed: `"Mode" : "dev"` which deprecates a high level dev mode, now replaced by granular controls
- Renamed: `"AllowTesting" : false` to `"EnableTesting": false` which allows the use of `/loadtest` slash commands during development
- Removed: `"UseSSL": false` boolean replaced by `"ConnectionSecurity": ""` under `Security` with new options: _None_ (`""`), _TLS_ (`"TLS"`) and _StartTLS_ ('"StartTLS"`)
- Renamed: `"Port": "8065"` to `"ListenAddress": ":8065"` to define address on which to listen. Must be prepended with a colon.
- Removed: `"Version": "developer"` removed and version information now stored in `model/version.go`
- Removed: `"Shards": {}` which was not used
- Moved: `"InviteSalt": "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"` to `EmailSettings`
- Moved: `"PublicLinkSalt": "TO3pTyXIZzwHiwyZgGql7lM7DG3zeId4"` to `FileSettings`
- Renamed and Moved `"ResetSalt": "IPxFzSfnDFsNsRafZxz8NaYqFKhf9y2t"` to `"PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL"` and moved to `EmailSettings`
- Removed: `"AnalyticsUrl": ""` which was not used
- Removed: `"UseLocalStorage": true` which is replaced by `"DriverName": "local"` in `FileSettings`
- Renamed and Moved: `"StorageDirectory": "./data/"` to `Directory` and moved to `FileSettings`
- Renamed: `"AllowedLoginAttempts": 10` to `"MaximumLoginAttempts": 10`
- Renamed, Reversed and Moved: `"DisableEmailSignUp": false` renamed `"EnableSignUpWithEmail": true`, reversed meaning of `true`, and moved to `EmailSettings`
- Added: `"EnableOAuthServiceProvider": false` to enable OAuth2 service provider functionality
- Added: `"EnableIncomingWebhooks": false` to enable incoming webhooks feature
##### Team Settings
- Under `TeamSettings` in [`config.json`](./config/config.json):
- **Renamed:** `"AllowPublicLink": true` renamed to `"EnablePublicLink": true` and moved to `FileSettings`
- **Removed:** `AllowValetDefault` which was a guest account feature that is deprecated
- **Removed:** `"TermsLink": "/static/help/configure_links.html"` removed since option didn't need configuration
- **Removed:** `"PrivacyLink": "/static/help/configure_links.html"` removed since option didn't need configuration
- **Removed:** `"AboutLink": "/static/help/configure_links.html"` removed since option didn't need configuration
- **Removed:** `"HelpLink": "/static/help/configure_links.html"` removed since option didn't need configuration
- **Removed:** `"ReportProblemLink": "/static/help/configure_links.html"` removed since option didn't need configuration
- **Removed:** `"TourLink": "/static/help/configure_links.html"` removed since option didn't need configuration
- **Removed:** `"DefaultThemeColor": "#2389D7"` removed since theme colors changed from 1 to 18, default theme color option may be added back later after theme color design stablizes
- **Renamed:** `"DisableTeamCreation": false` to `"EnableUserCreation": true` and reversed
- **Added:** ` "EnableUserCreation": true` added to disable ability to create new user accounts in the system
- Renamed: `"AllowPublicLink": true` renamed to `"EnablePublicLink": true` and moved to `FileSettings`
- Removed: `AllowValetDefault` which was a guest account feature that is deprecated
- Removed: `"TermsLink": "/static/help/configure_links.html"` removed since option didn't need configuration
- Removed: `"PrivacyLink": "/static/help/configure_links.html"` removed since option didn't need configuration
- Removed: `"AboutLink": "/static/help/configure_links.html"` removed since option didn't need configuration
- Removed: `"HelpLink": "/static/help/configure_links.html"` removed since option didn't need configuration
- Removed: `"ReportProblemLink": "/static/help/configure_links.html"` removed since option didn't need configuration
- Removed: `"TourLink": "/static/help/configure_links.html"` removed since option didn't need configuration
- Removed: `"DefaultThemeColor": "#2389D7"` removed since theme colors changed from 1 to 18, default theme color option may be added back later after theme color design stablizes
- Renamed: `"DisableTeamCreation": false` to `"EnableUserCreation": true` and reversed
- Added: ` "EnableUserCreation": true` added to disable ability to create new user accounts in the system
##### SSO Settings
- Under `SSOSettings` in [`config.json`](./config/config.json):
- **Renamed Category:** `SSOSettings` to `GitLabSettings`
- **Renamed:** `"Allow": false` to `"Enable": false` to enable GitLab SSO
- Renamed Category: `SSOSettings` to `GitLabSettings`
- Renamed: `"Allow": false` to `"Enable": false` to enable GitLab SSO
##### AWS Settings
- Under `AWSSettings` in [`config.json`](./config/config.json):
- This section was removed and settings moved to `FileSettings`
- **Renamed and Moved:** `"S3AccessKeyId": ""` renamed `"AmazonS3AccessKeyId": "",` and moved to `FileSettings`
- **Renamed and Moved:** `"S3SecretAccessKey": ""` renamed `"AmazonS3SecretAccessKey": "",` and moved to `FileSettings`
- **Renamed and Moved:** `"S3Bucket": ""` renamed `"AmazonS3Bucket": "",` and moved to `FileSettings`
- **Renamed and Moved:** `"S3Region": ""` renamed `"AmazonS3Region": "",` and moved to `FileSettings`
- Renamed and Moved: `"S3AccessKeyId": ""` renamed `"AmazonS3AccessKeyId": "",` and moved to `FileSettings`
- Renamed and Moved: `"S3SecretAccessKey": ""` renamed `"AmazonS3SecretAccessKey": "",` and moved to `FileSettings`
- Renamed and Moved: `"S3Bucket": ""` renamed `"AmazonS3Bucket": "",` and moved to `FileSettings`
- Renamed and Moved: `"S3Region": ""` renamed `"AmazonS3Region": "",` and moved to `FileSettings`
##### Image Settings
- Under `ImageSettings` in [`config.json`](./config/config.json):
- **Renamed:** `"ImageSettings"` section to `"FileSettings"`
- **Added:** `"DriverName" : "local"` to specify the file storage method, `amazons3` can also be used to setup S3
- Renamed: `"ImageSettings"` section to `"FileSettings"`
- Added: `"DriverName" : "local"` to specify the file storage method, `amazons3` can also be used to setup S3
##### EmailSettings
- Under `EmailSettings` in [`config.json`](./config/config.json):
- **Removed:** `"ByPassEmail": "true"` which is replaced with `SendEmailNotifications` and `RequireEmailVerification`
- **Added:** `"SendEmailNotifications" : "false"` to control whether email notifications are sent
- **Added:** `"RequireEmailVerification" : "false"` to control if users need to verify their emails
- **Replaced:** `"UseTLS": "false"` with `"ConnectionSecurity": ""` with options: _None_ (`""`), _TLS_ (`"TLS"`) and _StartTLS_ ('"StartTLS"`)
- **Replaced:** `"UseStartTLS": "false"` with `"ConnectionSecurity": ""` with options: _None_ (`""`), _TLS_ (`"TLS"`) and _StartTLS_ ('"StartTLS"`)
- Removed: `"ByPassEmail": "true"` which is replaced with `SendEmailNotifications` and `RequireEmailVerification`
- Added: `"SendEmailNotifications" : "false"` to control whether email notifications are sent
- Added: `"RequireEmailVerification" : "false"` to control if users need to verify their emails
- Replaced: `"UseTLS": "false"` with `"ConnectionSecurity": ""` with options: _None_ (`""`), _TLS_ (`"TLS"`) and _StartTLS_ (`"StartTLS"`)
- Replaced: `"UseStartTLS": "false"` with `"ConnectionSecurity": ""` with options: _None_ (`""`), _TLS_ (`"TLS"`) and _StartTLS_ (`"StartTLS"`)
##### Privacy Settings
- Under `PrivacySettings` in [`config.json`](./config/config.json):
- **Removed:** `"ShowPhoneNumber": "true"` which was not used
- **Removed:** `"ShowSkypeId" : "true"` which was not used
- Removed: `"ShowPhoneNumber": "true"` which was not used
- Removed: `"ShowSkypeId" : "true"` which was not used
### Database Changes from v0.7 to v1.0

View File

@@ -22,6 +22,7 @@ var commands = []commandHandler{
joinCommand,
loadTestCommand,
echoCommand,
shrugCommand,
}
var echoSem chan bool
@@ -160,6 +161,34 @@ func echoCommand(c *Context, command *model.Command) bool {
return false
}
func shrugCommand(c *Context, command *model.Command) bool {
cmd := "/shrug"
if !command.Suggest && strings.Index(command.Command, cmd) == 0 {
message := "¯\\_(ツ)_/¯"
parameters := strings.SplitN(command.Command, " ", 2)
if len(parameters) > 1 {
message += " " + parameters[1]
}
post := &model.Post{}
post.Message = message
post.ChannelId = command.ChannelId
if _, err := CreatePost(c, post, false); err != nil {
l4g.Error("Unable to create /shrug post post, err=%v", err)
return false
}
command.Response = model.RESP_EXECUTED
return true
} else if strings.Index(cmd, command.Command) == 0 {
command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: "Adds ¯\\_(ツ)_/¯ to your message, /shrug [message]"})
}
return false
}
func joinCommand(c *Context, command *model.Command) bool {
// looks for "/join channel-name"

View File

@@ -23,6 +23,7 @@ import (
"image/jpeg"
"io"
"io/ioutil"
"mime"
"net/http"
"net/url"
"os"
@@ -331,9 +332,18 @@ func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "max-age=2592000, public")
var mimeType string
ext := filepath.Ext(filename)
if model.IsFileExtImage(ext) {
mimeType = model.GetImageMimeType(ext)
} else {
mimeType = mime.TypeByExtension(ext)
}
result := make(map[string]string)
result["filename"] = filename
result["size"] = size
result["mime"] = mimeType
w.Write([]byte(model.MapToJson(result)))
}

View File

@@ -108,7 +108,7 @@ func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) {
team.Name = model.CleanTeamName(team.Name)
if err := team.IsValid(); err != nil {
if err := team.IsValid(*utils.Cfg.TeamSettings.RestrictTeamNames); err != nil {
c.Err = err
return
}
@@ -164,7 +164,7 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) {
teamSignup.Team.PreSave()
if err := teamSignup.Team.IsValid(); err != nil {
if err := teamSignup.Team.IsValid(*utils.Cfg.TeamSettings.RestrictTeamNames); err != nil {
c.Err = err
return
}
@@ -379,11 +379,6 @@ func FindTeamByName(c *Context, name string, all string) bool {
return false
}
if model.IsReservedTeamName(name) {
c.Err = model.NewAppError("findTeamByName", "This URL is unavailable. Please try another.", "name="+name)
return false
}
if result := <-Srv.Store.Team().GetByName(name); result.Err != nil {
return false
} else {

View File

@@ -17,7 +17,8 @@
"MaxUsersPerTeam": 50,
"EnableTeamCreation": true,
"EnableUserCreation": true,
"RestrictCreationToDomains": ""
"RestrictCreationToDomains": "",
"RestrictTeamNames": true
},
"SqlSettings": {
"DriverName": "mysql",

11
doc/help/Search.md Normal file
View File

@@ -0,0 +1,11 @@
# Search
The search box in Mattermost brings back results from any channel of which youre a member. No results are returned from channels where you are not a member - even if they are open channels.
Some things to know about search:
- Multiple search terms are connected with “OR” by default. Typing in `Mattermost website` returns results containing “Mattermost” or “website”
- You can use quotes to return search results for exact terms, like `"Mattermost website"` will only return messages containing the entire phrase `"Mattermost website"` and not return messages with only `Mattermost` or `website`
- You can use the `*` character for wildcard searches that match within words. For example: Searching for `rea*` brings back messages containing `reach`, `reason` and other words starting with `rea`.
Search in Mattermost uses the full text search features in MySQL and Postgres databases. Special cases that are not supported in default full text search, such as searching for IP addresses like `10.100.200.101`, can be added in future as the search feature evolves.

View File

@@ -1,12 +1,16 @@
### Mattermost Troubleshooting
# Mattermost Troubleshooting
#### Important notes
1. **DO NOT manipulate the Mattermost database**
##### **DO NOT manipulate the Mattermost database**
- In particular, DO NOT delete data from the database, as Mattermost is designed to stop working if data integrity has been compromised. The system is designed to archive content continously and generally assumes data is never deleted.
#### Common Issues
1. Error message in logs when attempting to sign-up: `x509: certificate signed by unknown authority`
##### Error message in logs when attempting to sign-up: `x509: certificate signed by unknown authority`
- This error may appear when attempt to use a self-signed certificate to setup SSL, which is not yet supported by Mattermost. You can resolve this issue by setting up a load balancer like Ngnix. A ticket exists to [add support for self-signed certificates in future](x509: certificate signed by unknown authority).
##### Lost System Administrator account
- If the System Administrator account becomes unavailable, a person leaving the organization for example, you can set a new system admin from the commandline using `./platform -assign_role -team_name="yourteam" -email="you@example.com" -role="system_admin"`

View File

@@ -1,12 +1,14 @@
# Mattermost Upgrade Guide
### Upgrading Mattermost v0.7 to v1.1
### Upgrading Mattermost v0.7 to v1.1.1
If you've manually changed Mattermost v0.7 configuration by updating the `config.json` file, you'll need to port those changes to Mattermost v1.1:
_Note: [Mattermost v1.1.1](https://github.com/mattermost/platform/releases/tag/v1.1.1) is a special release of Mattermost v1.1 that upgrades the database to Mattermost v1.1 from EITHER Mattermost v0.7 or Mattermost v1.0. The following instructions are for upgrading from Mattermost v0.7 to v1.1.1 and skipping the upgrade to Mattermost v1.0._
If you've manually changed Mattermost v0.7 configuration by updating the `config.json` file, you'll need to port those changes to Mattermost v1.1.1:
1. Go to the `config.json` file that you manually updated and note any differences from the [default `config.json` file in Mattermost 0.7](https://github.com/mattermost/platform/blob/v0.7.0/config/config.json).
2. For each setting that you changed, check [the changelog documentation](https://github.com/mattermost/platform/blob/master/CHANGELOG.md#configjson-changes-from-v07-to-v10) on whether the configuration setting has changed between v0.7 and v1.1
2. For each setting that you changed, check [the changelog documentation](https://github.com/mattermost/platform/blob/master/CHANGELOG.md#configjson-changes-from-v07-to-v10) on whether the configuration setting has changed between v0.7 and v1.1.1
3. Update your new [`config.json` file in Mattermost v1.1](https://github.com/mattermost/platform/blob/v1.1.0/config/config.json), based on your preferences and the changelog documentation above.

View File

@@ -122,6 +122,7 @@ type TeamSettings struct {
EnableTeamCreation bool
EnableUserCreation bool
RestrictCreationToDomains string
RestrictTeamNames *bool
}
type Config struct {
@@ -169,6 +170,11 @@ func (o *Config) SetDefaults() {
o.ServiceSettings.EnableSecurityFixAlert = new(bool)
*o.ServiceSettings.EnableSecurityFixAlert = true
}
if o.TeamSettings.RestrictTeamNames == nil {
o.TeamSettings.RestrictTeamNames = new(bool)
*o.TeamSettings.RestrictTeamNames = true
}
}
func (o *Config) IsValid() *AppError {

View File

@@ -97,7 +97,7 @@ func (o *Team) Etag() string {
return Etag(o.Id, o.UpdateAt)
}
func (o *Team) IsValid() *AppError {
func (o *Team) IsValid(restrictTeamNames bool) *AppError {
if len(o.Id) != 26 {
return NewAppError("Team.IsValid", "Invalid Id", "")
@@ -127,7 +127,7 @@ func (o *Team) IsValid() *AppError {
return NewAppError("Team.IsValid", "Invalid URL Identifier", "id="+o.Id)
}
if IsReservedTeamName(o.Name) {
if restrictTeamNames && IsReservedTeamName(o.Name) {
return NewAppError("Team.IsValid", "This URL is unavailable. Please try another.", "id="+o.Id)
}

View File

@@ -21,45 +21,45 @@ func TestTeamJson(t *testing.T) {
func TestTeamIsValid(t *testing.T) {
o := Team{}
if err := o.IsValid(); err == nil {
if err := o.IsValid(true); err == nil {
t.Fatal("should be invalid")
}
o.Id = NewId()
if err := o.IsValid(); err == nil {
if err := o.IsValid(true); err == nil {
t.Fatal("should be invalid")
}
o.CreateAt = GetMillis()
if err := o.IsValid(); err == nil {
if err := o.IsValid(true); err == nil {
t.Fatal("should be invalid")
}
o.UpdateAt = GetMillis()
if err := o.IsValid(); err == nil {
if err := o.IsValid(true); err == nil {
t.Fatal("should be invalid")
}
o.Email = strings.Repeat("01234567890", 20)
if err := o.IsValid(); err == nil {
if err := o.IsValid(true); err == nil {
t.Fatal("should be invalid")
}
o.Email = "corey@hulen.com"
o.DisplayName = strings.Repeat("01234567890", 20)
if err := o.IsValid(); err == nil {
if err := o.IsValid(true); err == nil {
t.Fatal("should be invalid")
}
o.DisplayName = "1234"
o.Name = "ZZZZZZZ"
if err := o.IsValid(); err == nil {
if err := o.IsValid(true); err == nil {
t.Fatal("should be invalid")
}
o.Name = "zzzzz"
o.Type = TEAM_OPEN
if err := o.IsValid(); err != nil {
if err := o.IsValid(true); err != nil {
t.Fatal(err)
}
}

View File

@@ -5,6 +5,7 @@ package store
import (
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
)
type SqlTeamStore struct {
@@ -52,7 +53,7 @@ func (s SqlTeamStore) Save(team *model.Team) StoreChannel {
team.PreSave()
if result.Err = team.IsValid(); result.Err != nil {
if result.Err = team.IsValid(*utils.Cfg.TeamSettings.RestrictTeamNames); result.Err != nil {
storeChannel <- result
close(storeChannel)
return
@@ -84,7 +85,7 @@ func (s SqlTeamStore) Update(team *model.Team) StoreChannel {
team.PreUpdate()
if result.Err = team.IsValid(); result.Err != nil {
if result.Err = team.IsValid(*utils.Cfg.TeamSettings.RestrictTeamNames); result.Err != nil {
storeChannel <- result
close(storeChannel)
return

View File

@@ -182,6 +182,7 @@ func getClientConfig(c *model.Config) map[string]string {
props["SiteName"] = c.TeamSettings.SiteName
props["EnableTeamCreation"] = strconv.FormatBool(c.TeamSettings.EnableTeamCreation)
props["RestrictTeamNames"] = strconv.FormatBool(*c.TeamSettings.RestrictTeamNames)
props["EnableOAuthServiceProvider"] = strconv.FormatBool(c.ServiceSettings.EnableOAuthServiceProvider)

View File

@@ -31,6 +31,7 @@ export default class TeamSettings extends React.Component {
config.TeamSettings.RestrictCreationToDomains = ReactDOM.findDOMNode(this.refs.RestrictCreationToDomains).value.trim();
config.TeamSettings.EnableTeamCreation = ReactDOM.findDOMNode(this.refs.EnableTeamCreation).checked;
config.TeamSettings.EnableUserCreation = ReactDOM.findDOMNode(this.refs.EnableUserCreation).checked;
config.TeamSettings.RestrictTeamNames = ReactDOM.findDOMNode(this.refs.RestrictTeamNames).checked;
var MaxUsersPerTeam = 50;
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxUsersPerTeam).value, 10))) {
@@ -208,6 +209,39 @@ export default class TeamSettings extends React.Component {
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='RestrictTeamNames'
>
{'Restrict Team Names: '}
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
<input
type='radio'
name='RestrictTeamNames'
value='true'
ref='RestrictTeamNames'
defaultChecked={this.props.config.TeamSettings.RestrictTeamNames}
onChange={this.handleChange}
/>
{'true'}
</label>
<label className='radio-inline'>
<input
type='radio'
name='RestrictTeamNames'
value='false'
defaultChecked={!this.props.config.TeamSettings.RestrictTeamNames}
onChange={this.handleChange}
/>
{'false'}
</label>
<p className='help-text'>{'When true, You cannot create a team name with reserved words like www, admin, support, test, channel, etc'}</p>
</div>
</div>
<div className='form-group'>
<div className='col-sm-12'>
{serverError}

View File

@@ -13,8 +13,10 @@ const MsgTyping = require('./msg_typing.jsx');
const FileUpload = require('./file_upload.jsx');
const FilePreview = require('./file_preview.jsx');
const Utils = require('../utils/utils.jsx');
const Constants = require('../utils/constants.jsx');
const ActionTypes = Constants.ActionTypes;
const KeyCodes = Constants.KeyCodes;
export default class CreateComment extends React.Component {
constructor(props) {
@@ -25,6 +27,7 @@ export default class CreateComment extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.commentMsgKeyPress = this.commentMsgKeyPress.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
this.handleArrowUp = this.handleArrowUp.bind(this);
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
@@ -158,6 +161,26 @@ export default class CreateComment extends React.Component {
$('.post-right__scroll').perfectScrollbar('update');
this.setState({messageText: messageText});
}
handleArrowUp(e) {
if (e.keyCode === KeyCodes.UP && this.state.messageText === '') {
e.preventDefault();
const channelId = ChannelStore.getCurrentId();
const lastPost = PostStore.getCurrentUsersLatestPost(channelId, this.props.rootId);
if (!lastPost) {
return;
}
AppDispatcher.handleViewAction({
type: ActionTypes.RECIEVED_EDIT_POST,
refocusId: '#reply_textbox',
title: 'Comment',
message: lastPost.message,
postId: lastPost.id,
channelId: lastPost.channel_id
});
}
}
handleUploadStart(clientIds) {
let draft = PostStore.getCommentDraft(this.props.rootId);
@@ -290,6 +313,7 @@ export default class CreateComment extends React.Component {
<Textbox
onUserInput={this.handleUserInput}
onKeyPress={this.commentMsgKeyPress}
onKeyDown={this.handleArrowUp}
messageText={this.state.messageText}
createMessage='Add a comment...'
initialText=''

View File

@@ -10,9 +10,12 @@ export default class FileAttachment extends React.Component {
super(props);
this.loadFiles = this.loadFiles.bind(this);
this.playGif = this.playGif.bind(this);
this.stopGif = this.stopGif.bind(this);
this.addBackgroundImage = this.addBackgroundImage.bind(this);
this.canSetState = false;
this.state = {fileSize: -1};
this.state = {fileSize: -1, mime: '', playing: false, loading: false, format: ''};
}
componentDidMount() {
this.loadFiles();
@@ -28,15 +31,9 @@ export default class FileAttachment extends React.Component {
var filename = this.props.filename;
if (filename) {
var fileInfo = utils.splitFileLocation(filename);
var fileInfo = this.getFileInfoFromName(filename);
var type = utils.getFileType(fileInfo.ext);
// This is a temporary patch to fix issue with old files using absolute paths
if (fileInfo.path.indexOf('/api/v1/files/get') !== -1) {
fileInfo.path = fileInfo.path.split('/api/v1/files/get')[1];
}
fileInfo.path = utils.getWindowLocationOrigin() + '/api/v1/files/get' + fileInfo.path;
if (type === 'image') {
var self = this; // Need this reference since we use the given "this"
$('<img/>').attr('src', fileInfo.path + '_thumb.jpg?' + utils.getSessionIndex()).load(function loadWrapper(path, name) {
@@ -58,11 +55,7 @@ export default class FileAttachment extends React.Component {
$(imgDiv).addClass('normal');
}
var re1 = new RegExp(' ', 'g');
var re2 = new RegExp('\\(', 'g');
var re3 = new RegExp('\\)', 'g');
var url = path.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29');
$(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg?' + utils.getSessionIndex() + ')');
self.addBackgroundImage(name, path);
}
};
}(fileInfo.path, filename));
@@ -93,6 +86,75 @@ export default class FileAttachment extends React.Component {
return true;
}
playGif(e, filename) {
var img = new Image();
var fileUrl = utils.getFileUrl(filename);
this.setState({loading: true});
img.load(fileUrl);
img.onload = () => {
var state = {playing: true, loading: false};
switch (true) {
case img.width > img.height:
state.format = 'landscape';
break;
case img.height > img.width:
state.format = 'portrait';
break;
default:
state.format = 'quadrat';
break;
}
this.setState(state);
// keep displaying background image for a short moment while browser is
// loading gif, to prevent white background flashing through
setTimeout(() => this.removeBackgroundImage.bind(this)(filename), 100);
};
img.onError = () => this.setState({loading: false});
e.stopPropagation();
}
stopGif(e, filename) {
this.setState({playing: false});
this.addBackgroundImage(filename);
e.stopPropagation();
}
getFileInfoFromName(name) {
var fileInfo = utils.splitFileLocation(name);
// This is a temporary patch to fix issue with old files using absolute paths
if (fileInfo.path.indexOf('/api/v1/files/get') !== -1) {
fileInfo.path = fileInfo.path.split('/api/v1/files/get')[1];
}
fileInfo.path = utils.getWindowLocationOrigin() + '/api/v1/files/get' + fileInfo.path;
return fileInfo;
}
addBackgroundImage(name, path) {
var fileUrl = path;
if (name in this.refs) {
if (!path) {
fileUrl = this.getFileInfoFromName(name).path;
}
var imgDiv = ReactDOM.findDOMNode(this.refs[name]);
var re1 = new RegExp(' ', 'g');
var re2 = new RegExp('\\(', 'g');
var re3 = new RegExp('\\)', 'g');
var url = fileUrl.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29');
$(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg?' + utils.getSessionIndex() + ')');
}
}
removeBackgroundImage(name) {
if (name in this.refs) {
$(ReactDOM.findDOMNode(this.refs[name])).css('background-image', 'initial');
}
}
render() {
var filename = this.props.filename;
@@ -100,15 +162,71 @@ export default class FileAttachment extends React.Component {
var fileUrl = utils.getFileUrl(filename);
var type = utils.getFileType(fileInfo.ext);
var thumbnail;
if (type === 'image') {
thumbnail = (
var playbackControls = '';
var loadedFile = '';
var loadingIndicator = '';
if (this.state.mime === 'image/gif') {
playbackControls = (
<div
ref={filename}
className='post__load'
style={{backgroundImage: 'url(/static/images/load.gif)'}}
className='file-playback-controls play'
onClick={(e) => this.playGif(e, filename)}
>
{"►"}
</div>
);
}
if (this.state.playing) {
loadedFile = (
<img
className={'file__loaded ' + this.state.format}
src={fileUrl}
/>
);
playbackControls = (
<div
className='file-playback-controls stop'
onClick={(e) => this.stopGif(e, filename)}
>
{"■"}
</div>
);
}
if (this.state.loading) {
loadingIndicator = (
<img
className='spinner file__loading'
src='/static/images/load.gif'
/>
);
playbackControls = '';
}
var thumbnail;
if (type === 'image') {
if (this.state.playing) {
thumbnail = (
<div
ref={filename}
className='post__load'
style={{backgroundImage: 'url(/static/images/load.gif)'}}
>
{playbackControls}
{loadedFile}
</div>
);
} else {
thumbnail = (
<div
ref={filename}
className='post__load'
style={{backgroundImage: 'url(/static/images/load.gif)'}}
>
{loadingIndicator}
{playbackControls}
{loadedFile}
</div>
);
}
} else {
thumbnail = <div className={'file-icon ' + utils.getIconClassName(type)}/>;
}
@@ -119,7 +237,7 @@ export default class FileAttachment extends React.Component {
filename,
function success(data) {
if (this.canSetState) {
this.setState({fileSize: parseInt(data.size, 10)});
this.setState({fileSize: parseInt(data.size, 10), mime: data.mime});
}
}.bind(this),
function error() {}

View File

@@ -12,9 +12,19 @@ export default class FileUploadOverlay extends React.Component {
return (
<div className={overlayClass}>
<div>
<i className='fa fa-upload'></i>
<span>Drop a file to upload it.</span>
<div className='overlay__circle'>
<img
className='overlay__files'
src='/static/images/filesOverlay.png'
alt='Files'
/>
<span><i className='fa fa-upload'></i>{'Drop a file to upload it.'}</span>
<img
className='overlay__logo'
src='/static/images/logoWhite.png'
width='100'
alt='Logo'
/>
</div>
</div>
);

View File

@@ -40,10 +40,12 @@ export default class TeamSignupUrlPage extends React.Component {
return;
}
for (let index = 0; index < Constants.RESERVED_TEAM_NAMES.length; index++) {
if (cleanedName.indexOf(Constants.RESERVED_TEAM_NAMES[index]) === 0) {
this.setState({nameError: 'URL is taken or contains a reserved word'});
return;
if (global.window.mm_config.RestrictTeamNames === 'true') {
for (let index = 0; index < Constants.RESERVED_TEAM_NAMES.length; index++) {
if (cleanedName.indexOf(Constants.RESERVED_TEAM_NAMES[index]) === 0) {
this.setState({nameError: 'URL is taken or contains a reserved word'});
return;
}
}
}

View File

@@ -38,7 +38,10 @@ export default class ViewImageModal extends React.Component {
progress: progress,
images: {},
fileSizes: {},
showFooter: false
fileMimes: {},
showFooter: false,
isPlaying: {},
isLoading: {}
};
}
handleNext(e) {
@@ -122,6 +125,36 @@ export default class ViewImageModal extends React.Component {
this.setState({loaded});
}
}
playGif(e, filename, fileUrl) {
var isLoading = this.state.isLoading;
var isPlaying = this.state.isPlaying;
isLoading[filename] = fileUrl;
this.setState({isLoading});
var img = new Image();
img.load(fileUrl);
img.onload = () => {
delete isLoading[filename];
isPlaying[filename] = fileUrl;
this.setState({isPlaying, isLoading});
};
img.onError = () => {
delete isLoading[filename];
this.setState({isLoading});
};
e.stopPropagation();
e.preventDefault();
}
stopGif(e, filename) {
var isPlaying = this.state.isPlaying;
delete isPlaying[filename];
this.setState({isPlaying});
e.stopPropagation();
e.preventDefault();
}
componentDidMount() {
$(window).on('keyup', this.handleKeyPress);
@@ -154,6 +187,10 @@ export default class ViewImageModal extends React.Component {
var fileType = Utils.getFileType(fileInfo.ext);
if (fileType === 'image') {
if (filename in this.state.isPlaying) {
return this.state.isPlaying[filename];
}
// This is a temporary patch to fix issue with old files using absolute paths
if (fileInfo.path.indexOf('/api/v1/files/get') !== -1) {
fileInfo.path = fileInfo.path.split('/api/v1/files/get')[1];
@@ -189,12 +226,62 @@ export default class ViewImageModal extends React.Component {
var fileType = Utils.getFileType(fileInfo.ext);
if (fileType === 'image') {
if (!(filename in this.state.fileMimes)) {
Client.getFileInfo(
filename,
(data) => {
if (this.canSetState) {
var fileMimes = this.state.fileMimes;
fileMimes[filename] = data.mime;
this.setState(fileMimes);
}
},
() => {}
);
}
var playbackControls = '';
if (this.state.fileMimes[filename] === 'image/gif' && !(filename in this.state.isLoading)) {
if (filename in this.state.isPlaying) {
playbackControls = (
<div
className='file-playback-controls stop'
onClick={(e) => this.stopGif(e, filename)}
>
{"■"}
</div>
);
} else {
playbackControls = (
<div
className='file-playback-controls play'
onClick={(e) => this.playGif(e, filename, fileUrl)}
>
{"►"}
</div>
);
}
}
var loadingIndicator = '';
if (this.state.isLoading[filename] === fileUrl) {
loadingIndicator = (
<img
className='spinner file__loading'
src='/static/images/load.gif'
/>
);
playbackControls = '';
}
// image files just show a preview of the file
content = (
<a
href={fileUrl}
target='_blank'
>
{loadingIndicator}
{playbackControls}
<img
style={{maxHeight: this.state.imgHeight}}
ref='image'

View File

@@ -230,7 +230,7 @@ class PostStoreClass extends EventEmitter {
getPosts(channelId) {
return BrowserStore.getItem('posts_' + channelId);
}
getCurrentUsersLatestPost(channelId) {
getCurrentUsersLatestPost(channelId, rootId) {
const userId = UserStore.getCurrentId();
var postList = makePostListNonNull(this.getPosts(channelId));
var i = 0;
@@ -239,8 +239,15 @@ class PostStoreClass extends EventEmitter {
for (i; i < len; i++) {
if (postList.posts[postList.order[i]].user_id === userId) {
lastPost = postList.posts[postList.order[i]];
break;
if (rootId) {
if (postList.posts[postList.order[i]].root_id === rootId || postList.posts[postList.order[i]].id === rootId) {
lastPost = postList.posts[postList.order[i]];
break;
}
} else {
lastPost = postList.posts[postList.order[i]];
break;
}
}
}

View File

@@ -133,7 +133,7 @@ export function handleEmoticons(text, tokens) {
const alias = `MM_EMOTICON${index}`;
tokens.set(alias, {
value: `<img align="absmiddle" alt=${match} class="emoji" src=${getImagePathForEmoticon(name)} title=${match} />`,
value: `<img align="absmiddle" alt="${match}" class="emoji" src="${getImagePathForEmoticon(name)}" title="${match}" />`,
originalText: match
});

View File

@@ -11,6 +11,7 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
super(options);
this.heading = this.heading.bind(this);
this.paragraph = this.paragraph.bind(this);
this.text = this.text.bind(this);
this.formattingOptions = formattingOptions;
@@ -53,11 +54,17 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
}
paragraph(text) {
if (this.formattingOptions.singleline) {
return `<p class="markdown__paragraph-inline">${text}</p>`;
let outText = text;
if (!('emoticons' in this.options) || this.options.emoticon) {
outText = TextFormatting.doFormatEmoticons(text);
}
return super.paragraph(text);
if (this.formattingOptions.singleline) {
return `<p class="markdown__paragraph-inline">${outText}</p>`;
}
return super.paragraph(outText);
}
table(header, body) {

View File

@@ -69,6 +69,15 @@ export function doFormatText(text, options) {
return output;
}
export function doFormatEmoticons(text) {
const tokens = new Map();
let output = Emoticons.handleEmoticons(text, tokens);
output = replaceTokens(output, tokens);
return output;
}
export function sanitizeHtml(text) {
let output = text;

View File

@@ -544,6 +544,7 @@ export function applyTheme(theme) {
if (theme.buttonBg) {
changeCss('.btn.btn-primary', 'background:' + theme.buttonBg, 1);
changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background:' + changeColor(theme.buttonBg, -0.25), 1);
changeCss('.file-playback-controls', 'color:' + changeColor(theme.buttonBg, -0.25), 1);
}
if (theme.buttonColor) {

View File

@@ -133,12 +133,34 @@
height: 100%;
background-color: #FFF;
background-repeat: no-repeat;
overflow: hidden;
position: relative;
text-align: center;
&.small {
background-position: center;
}
&.normal {
background-position: top left;
}
.spinner.file__loading {
position: absolute;
left: 50%;
margin-left: -16px;
top: 50%;
margin-top: -16px;
}
.file__loaded {
max-width: initial;
&.landscape, &.quadrat {
height: 100px;
}
&.portrait {
width: 120px;
}
}
&:hover .file-playback-controls.stop {
@include opacity(1);
}
}
.post-image__thumbnail {
float: left;
@@ -215,3 +237,20 @@
}
}
}
.file-playback-controls {
position: absolute;
right: 5px;
bottom: 0;
font-size: 22px;
cursor: pointer;
z-index: 2;
-webkit-transition: opacity 0.6s;
-moz-transition: opacity 0.6s;
-o-transition: opacity 0.6s;
transition: opacity 0.6s;
&.stop {
@include opacity(0);
}
}

View File

@@ -228,11 +228,24 @@
background: #FFF;
display: table-cell;
vertical-align: middle;
position: relative;
&:hover .file-playback-controls.stop {
@include opacity(1);
}
}
img {
max-width: 100%;
max-height: 100%;
}
.spinner.file__loading {
z-index: 2;
position: absolute;
left: 50%;
margin-left: -16px;
top: 50%;
margin-top: -16px;
}
}
.modal-content{
box-shadow: none;

View File

@@ -119,20 +119,52 @@ body.ios {
background-color: rgba(0, 0, 0, 0.6);
text-align: center;
color: #FFF;
display: table;
font-size: 1.7em;
font-size: em(20px);
font-weight: 600;
z-index: 6;
> div {
display: table-cell;
vertical-align: middle;
&.right-file-overlay {
font-size: em(18px);
.overlay__circle {
width: 300px;
height: 300px;
margin: -150px 0 0 -150px;
}
.overlay__files {
margin: 60px auto 15px;
width: 150px;
}
}
.overlay__circle {
background: #111;
background: rgba(black, 0.7);
width: 370px;
height: 370px;
margin: -185px 0 0 -185px;
@include border-radius(500px);
position: absolute;
top: 50%;
left: 50%;
}
.overlay__files {
display: block;
margin: 75px auto 20px;
}
.overlay__logo {
position: absolute;
left: 50%;
bottom: 30px;
margin-left: -50px;
@include opacity(0.3);
}
.fa {
display: block;
font-size: 2em;
margin: 0 0 0.3em;
display: inline-block;
font-size: 1.1em;
margin-right: 8px;
}
}

View File

@@ -199,9 +199,6 @@
}
@media screen and (max-width: 960px) {
.center-file-overlay {
font-size: 1.5em;
}
.post {
.post-header .post-header-col.post-header__reply {
.comment-icon__container__hide {
@@ -278,8 +275,17 @@
display: block;
}
}
.center-file-overlay {
font-size: 1.3em;
.file-overlay {
font-size: em(18px);
.overlay__circle {
width: 300px;
height: 300px;
margin: -150px 0 0 -150px;
}
.overlay__files {
margin: 60px auto 15px;
width: 150px;
}
}
.date-separator, .new-separator {
&.hovered--after {
@@ -639,6 +645,9 @@
}
&.has-close {
.btn-close {
width: 40px;
text-align: center;
right: 0;
display: block;
@include opacity(0.5);
}
@@ -749,6 +758,10 @@
.post-comments {
padding: 9px 21px 10px 10px !important;
}
.post-image__column .post__image .file-playback-controls.stop, .image-wrapper > a .file-playback-controls.stop {
@include opacity(1);
}
}
@media screen and (max-width: 640px) {
.access-history__table {

View File

@@ -103,6 +103,9 @@
text-overflow: ellipsis;
margin-bottom: 0;
}
.input-group-addon {
background: transparent;
}
.radio {
label {
font-weight: 600;

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB