summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/command.go29
-rw-r--r--api/file.go10
-rw-r--r--api/team.go9
-rw-r--r--config/config.json3
-rw-r--r--doc/help/Search.md11
-rw-r--r--doc/install/Upgrade-Guide.md8
-rw-r--r--model/config.go6
-rw-r--r--model/team.go4
-rw-r--r--model/team_test.go16
-rw-r--r--store/sql_team_store.go5
-rw-r--r--utils/config.go1
-rw-r--r--web/react/components/admin_console/team_settings.jsx34
-rw-r--r--web/react/components/create_comment.jsx24
-rw-r--r--web/react/components/file_attachment.jsx158
-rw-r--r--web/react/components/team_signup_url_page.jsx10
-rw-r--r--web/react/components/view_image.jsx89
-rw-r--r--web/react/stores/post_store.jsx13
-rw-r--r--web/react/utils/utils.jsx1
-rw-r--r--web/sass-files/sass/partials/_files.scss39
-rw-r--r--web/sass-files/sass/partials/_modal.scss13
-rw-r--r--web/sass-files/sass/partials/_responsive.scss4
21 files changed, 436 insertions, 51 deletions
diff --git a/api/command.go b/api/command.go
index 52ff8fffd..54f863c48 100644
--- a/api/command.go
+++ b/api/command.go
@@ -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"
diff --git a/api/file.go b/api/file.go
index 142ef7ac7..94eea516a 100644
--- a/api/file.go
+++ b/api/file.go
@@ -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)))
}
diff --git a/api/team.go b/api/team.go
index f6038566a..2d7b05ff6 100644
--- a/api/team.go
+++ b/api/team.go
@@ -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 {
diff --git a/config/config.json b/config/config.json
index 37109428d..7bac58df7 100644
--- a/config/config.json
+++ b/config/config.json
@@ -17,7 +17,8 @@
"MaxUsersPerTeam": 50,
"EnableTeamCreation": true,
"EnableUserCreation": true,
- "RestrictCreationToDomains": ""
+ "RestrictCreationToDomains": "",
+ "RestrictTeamNames": true
},
"SqlSettings": {
"DriverName": "mysql",
diff --git a/doc/help/Search.md b/doc/help/Search.md
new file mode 100644
index 000000000..f36e079bd
--- /dev/null
+++ b/doc/help/Search.md
@@ -0,0 +1,11 @@
+# Search
+
+The search box in Mattermost brings back results from any channel of which you’re 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.
diff --git a/doc/install/Upgrade-Guide.md b/doc/install/Upgrade-Guide.md
index e86cf8166..cecd45353 100644
--- a/doc/install/Upgrade-Guide.md
+++ b/doc/install/Upgrade-Guide.md
@@ -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.
diff --git a/model/config.go b/model/config.go
index 3a39df2f1..216b1de86 100644
--- a/model/config.go
+++ b/model/config.go
@@ -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 {
diff --git a/model/team.go b/model/team.go
index 584c78f8d..9da2cd5b2 100644
--- a/model/team.go
+++ b/model/team.go
@@ -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)
}
diff --git a/model/team_test.go b/model/team_test.go
index fd2428f03..112d48a9d 100644
--- a/model/team_test.go
+++ b/model/team_test.go
@@ -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)
}
}
diff --git a/store/sql_team_store.go b/store/sql_team_store.go
index 2d65435b0..380d979bd 100644
--- a/store/sql_team_store.go
+++ b/store/sql_team_store.go
@@ -5,6 +5,7 @@ package store
import (
"github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
)
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
diff --git a/utils/config.go b/utils/config.go
index e3349650b..15d6b217c 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -182,6 +182,7 @@ func getClientProperties(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)
diff --git a/web/react/components/admin_console/team_settings.jsx b/web/react/components/admin_console/team_settings.jsx
index da4299714..9ecd14a1e 100644
--- a/web/react/components/admin_console/team_settings.jsx
+++ b/web/react/components/admin_console/team_settings.jsx
@@ -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))) {
@@ -209,6 +210,39 @@ export default class TeamSettings extends React.Component {
</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}
<button
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index 12d1af6ff..435c7d542 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -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=''
diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx
index c6dff6550..57cccc4e0 100644
--- a/web/react/components/file_attachment.jsx
+++ b/web/react/components/file_attachment.jsx
@@ -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').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)');
+ 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)');
+ }
+ }
+ 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() {}
diff --git a/web/react/components/team_signup_url_page.jsx b/web/react/components/team_signup_url_page.jsx
index 67e4c9dd7..75ec2dfd9 100644
--- a/web/react/components/team_signup_url_page.jsx
+++ b/web/react/components/team_signup_url_page.jsx
@@ -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.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;
+ }
}
}
diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx
index 322e68c17..bea6ce7a5 100644
--- a/web/react/components/view_image.jsx
+++ b/web/react/components/view_image.jsx
@@ -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'
diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx
index 8609d8bbf..0ace956d2 100644
--- a/web/react/stores/post_store.jsx
+++ b/web/react/stores/post_store.jsx
@@ -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;
+ }
}
}
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 5f266bba3..b9084b26e 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -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) {
diff --git a/web/sass-files/sass/partials/_files.scss b/web/sass-files/sass/partials/_files.scss
index 01057423d..d3ab3b9f8 100644
--- a/web/sass-files/sass/partials/_files.scss
+++ b/web/sass-files/sass/partials/_files.scss
@@ -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);
+ }
+}
diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss
index 5570b5ce4..1dcdbf348 100644
--- a/web/sass-files/sass/partials/_modal.scss
+++ b/web/sass-files/sass/partials/_modal.scss
@@ -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;
diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss
index 3bffe82a2..c8bb24f3a 100644
--- a/web/sass-files/sass/partials/_responsive.scss
+++ b/web/sass-files/sass/partials/_responsive.scss
@@ -758,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 {