summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md30
-rw-r--r--api/file.go13
-rw-r--r--api/user.go2
-rw-r--r--config/config.json4
-rw-r--r--config/config_docker.json4
-rw-r--r--scripts/README_DEV.md106
-rw-r--r--store/sql_post_store.go2
-rw-r--r--store/sql_post_store_test.go5
-rw-r--r--web/react/components/channel_loader.jsx3
-rw-r--r--web/react/components/create_post.jsx91
-rw-r--r--web/react/components/file_attachment.jsx11
-rw-r--r--web/react/components/member_list.jsx2
-rw-r--r--web/react/components/post.jsx2
-rw-r--r--web/react/components/post_list.jsx13
-rw-r--r--web/react/components/signup_team_complete.jsx46
-rw-r--r--web/react/components/user_settings.jsx203
-rw-r--r--web/react/stores/post_store.jsx17
-rw-r--r--web/react/utils/async_client.jsx12
-rw-r--r--web/react/utils/constants.jsx4
-rw-r--r--web/react/utils/utils.jsx2
-rw-r--r--web/sass-files/sass/partials/_files.scss7
-rw-r--r--web/sass-files/sass/partials/_post.scss6
22 files changed, 286 insertions, 299 deletions
diff --git a/README.md b/README.md
index 4ba3de128..55d1383a8 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-**Mattermost Alpha**
-**Team Communication Service**
+**Mattermost Alpha**
+**Team Communication Service**
**Development Build**
@@ -22,9 +22,9 @@ Learn More
Installing Mattermost
=====================
-You're installing "Mattermost Alpha", a pre-released version intended for an early look at what we're building. While SpinPunch runs this version internally, it's not recommended for production deployments since we can't guarantee API stability or backwards compatibility until our production release.
+You're installing "Mattermost Alpha", a pre-released version intended for an early look at what we're building. While SpinPunch runs this version internally, it's not recommended for production deployments since we can't guarantee API stability or backwards compatibility until our production release.
-That said, any issues at all, please let us know on the Mattermost forum at: http://forum.mattermost.org
+That said, any issues at all, please let us know on the Mattermost forum at: http://forum.mattermost.org
Local Machine Setup (Docker)
-----------------------------
@@ -77,7 +77,7 @@ Local Machine Setup (Docker)
### Notes ###
If your ISP blocks port 25 then you may install locally but email will not be sent.
-If you want to work with the latest bits in the repo you can run the cmd
+If you want to work with the latest bits in the repo you can run the cmd
`docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:latest`
You can update to the latest bits by running
@@ -113,13 +113,23 @@ AWS Elastic Beanstalk Setup (Docker)
14. Wait for beanstalk to update the environment.
15. Try it out by entering the domain of the form \*.elasticbeanstalk.com found at the top of the dashboard into your browser. You can also map your own domain if you wish.
-Contributing
-------------
-
-To contribute to this open source project please review the Mattermost Contribution Guidelines at http://www.mattermost.org/contribute-to-mattermost/.
+Configuration Settings
+----------------------
+
+There are a few configuration settings you might want to adjust when setting up your instance of Mattermost. You can edit them in ./config/config.json or ./config/config/config_docker.json if you're running a docker instance.
+
+* *EmailSettings*:*ByPassEmail* - If this is set to true, then users on the system will not need to verify their email addresses when signing up. In addition, no emails will ever be sent.
+* *ServiceSettings*:*UseLocalStorage* - If this is set to true, then your Mattermost server will store uploaded files in the storage directory specified by *StorageDirectory*. *StorageDirectory* must be set if *UseLocalStorage* is set to true.
+* *ServiceSettings*:*StorageDirectory* - The file path where files will be stored locally if *UseLocalStorage* is set to true. The operating system user that is running the Mattermost application must have read and write privileges to this directory.
+* *AWSSettings*:*S3*\* - If *UseLocalStorage* is set to false, and the S3 settings are configured here, then Mattermost will store files in the provided S3 bucket.
+
+Contributing
+------------
+
+To contribute to this open source project please review the Mattermost Contribution Guidelines at http://www.mattermost.org/contribute-to-mattermost/.
License
-------
-Mattermost is licensed under an "Apache-wrapped AGPL" model, which means 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.
+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.
diff --git a/api/file.go b/api/file.go
index 219cf6103..4ec421eb9 100644
--- a/api/file.go
+++ b/api/file.go
@@ -140,11 +140,18 @@ func fireAndForgetHandleImages(filenames []string, fileData [][]byte, teamId, ch
// Create thumbnail
go func() {
+ thumbWidth := float64(utils.Cfg.ImageSettings.ThumbnailWidth)
+ thumbHeight := float64(utils.Cfg.ImageSettings.ThumbnailHeight)
+ imgWidth := float64(imgConfig.Width)
+ imgHeight := float64(imgConfig.Height)
+
var thumbnail image.Image
- if imgConfig.Width > int(utils.Cfg.ImageSettings.ThumbnailWidth) {
- thumbnail = resize.Resize(utils.Cfg.ImageSettings.ThumbnailWidth, utils.Cfg.ImageSettings.ThumbnailHeight, img, resize.Lanczos3)
- } else {
+ if imgHeight < thumbHeight && imgWidth < thumbWidth {
thumbnail = img
+ } else if imgHeight/imgWidth < thumbHeight/thumbWidth {
+ thumbnail = resize.Resize(0, utils.Cfg.ImageSettings.ThumbnailHeight, img, resize.Lanczos3)
+ } else {
+ thumbnail = resize.Resize(utils.Cfg.ImageSettings.ThumbnailWidth, 0, img, resize.Lanczos3)
}
buf := new(bytes.Buffer)
diff --git a/api/user.go b/api/user.go
index ad5385695..54337e207 100644
--- a/api/user.go
+++ b/api/user.go
@@ -864,7 +864,7 @@ func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) {
}
if !model.ComparePassword(user.Password, currentPassword) {
- c.Err = model.NewAppError("updatePassword", "Update password failed because of invalid password", "")
+ c.Err = model.NewAppError("updatePassword", "The \"Current Password\" you entered is incorrect. Please check that Caps Lock is off and try again.", "")
c.Err.StatusCode = http.StatusForbidden
return
}
diff --git a/config/config.json b/config/config.json
index a1e73eb03..10a8b6553 100644
--- a/config/config.json
+++ b/config/config.json
@@ -49,8 +49,8 @@
"S3Region": ""
},
"ImageSettings": {
- "ThumbnailWidth": 200,
- "ThumbnailHeight": 0,
+ "ThumbnailWidth": 120,
+ "ThumbnailHeight": 100,
"PreviewWidth": 1024,
"PreviewHeight": 0,
"ProfileWidth": 128,
diff --git a/config/config_docker.json b/config/config_docker.json
index 6d220f919..c9fa5931a 100644
--- a/config/config_docker.json
+++ b/config/config_docker.json
@@ -49,8 +49,8 @@
"S3Region": ""
},
"ImageSettings": {
- "ThumbnailWidth": 200,
- "ThumbnailHeight": 0,
+ "ThumbnailWidth": 120,
+ "ThumbnailHeight": 100,
"PreviewWidth": 1024,
"PreviewHeight": 0,
"ProfileWidth": 128,
diff --git a/scripts/README_DEV.md b/scripts/README_DEV.md
index be24daad9..a088bbbc2 100644
--- a/scripts/README_DEV.md
+++ b/scripts/README_DEV.md
@@ -1,42 +1,82 @@
-Developer Machine Setup (Mac)
+Developer Machine Setup
-----------------------------
-DOCKER SETUP
+### Mac OS X ###
-1. Follow the instructions at http://docs.docker.com/installation/mac/
- 1. Use the Boot2Docker command-line utility
- 2. If you do command-line setup use: `boot2docker init eval “$(boot2docker shellinit)”`
-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
+1. Download and set up Boot2Docker
+ 1. Follow the instructions at http://docs.docker.com/installation/mac/
+ 1. Use the Boot2Docker command-line utility
+ 2. If you do command-line setup use: `boot2docker init eval “$(boot2docker shellinit)”`
+ 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/
+3. Set up your Go workspace
+ 1. `mkdir ~/go`
+ 2. Add the following to your ~/.bash_profile
+ `export GOPATH=$HOME/go`
+ `export PATH=$PATH:$GOPATH/bin`
+ 3. Reload your bash profile
+ `source ~/.bash_profile`
+4. Install Node.js using Homebrew
+ 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`
+6. Download Mattermost
+ `cd ~/go`
+ `mkdir -p src/github.com/mattermost`
+ `cd src/github.com/mattermost`
+ `git clone 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`
Any issues? Please let us know on our forums at: http://forum.mattermost.org
-GO SETUP
+### Ubuntu ###
-1. Download Go from http://golang.org/dl/
-
-NODE.JS SETUP
-
-1. Install homebrew from http://brew.sh
-2. `brew install node`
-
-COMPASS SETUP
-
-1. Make sure you have the latest version of Ruby
-2. `gem install compass`
-
-MATTERMOST SETUP
-
-1. Make a project directory for Mattermost, which we'll call **$PROJECT** for the rest of these instructions
-2. Make a `go` directory in your $PROJECT directory
-3. Open or create your ~/.bash_profile and add the following lines:
- `export GOPATH=$PROJECT/go`
- `export PATH=$PATH:$GOPATH/bin`
- then refresh your bash profile with `source ~/.bash_profile`
-4. Then use `cd $GOPATH` and `mkdir -p src/github.com/mattermost` then cd into this directory and run `git clone github.com/mattermost/platform.git`
-5. If you do not have Mercurial, download it with: `brew install mercurial`
-6. Then do `cd platform` and `make test`. Provided the test runs fine, you now have a complete build environment.
-7. Use `make run` to run your code
+1. Download Docker
+ 1. Follow the instructions at https://docs.docker.com/installation/ubuntulinux/ or use the summary below:
+ `sudo apt-get update`
+ `sudo apt-get install wget`
+ `wget -qO- https://get.docker.com/ | sh`
+ `sudo usermod -aG docker <username>`
+ `sudo service docker start`
+ `newgrp docker`
+2. Set up your dockerhost address
+ 1. Edit your /etc/hosts file to include the following line
+ `127.0.0.1 dockerhost`
+3. Install build essentials
+ 1. `apt-get install build-essential`
+4. Download Go 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
+ `export GOPATH=$HOME/go`
+ `export GOROOT=/usr/local/go`
+ `export PATH=$PATH:$GOROOT/bin`
+ 3. Reload your bashrc
+ `source ~/.bashrc`
+6. Install Node.js
+ 1. Download the newest version of the Node.js sources from https://nodejs.org/download/
+ 2. Extract the contents of the package and cd into the extracted files
+ 3. Compile and install Node.js
+ `./configure`
+ `make`
+ `make install`
+7. Install Ruby and Compass
+ `apt-get install ruby`
+ `apt-get install ruby-dev`
+ `gem install compass`
+8. Download Mattermost
+ `cd ~/go`
+ `mkdir -p src/github.com/mattermost`
+ `cd src/github.com/mattermost`
+ `git clone 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`
Any issues? Please let us know on our forums at: http://forum.mattermost.org
diff --git a/store/sql_post_store.go b/store/sql_post_store.go
index ede69d125..479caf838 100644
--- a/store/sql_post_store.go
+++ b/store/sql_post_store.go
@@ -401,7 +401,7 @@ func (s SqlPostStore) Search(teamId string, userId string, terms string, isHasht
Id = ChannelId AND TeamId = $1
AND UserId = $2
AND DeleteAt = 0)
- AND %s @@ plainto_tsquery($3)
+ AND %s @@ to_tsquery($3)
ORDER BY CreateAt DESC
LIMIT 100`, searchType)
diff --git a/store/sql_post_store_test.go b/store/sql_post_store_test.go
index d1639aa03..336a20d98 100644
--- a/store/sql_post_store_test.go
+++ b/store/sql_post_store_test.go
@@ -483,4 +483,9 @@ func TestPostStoreSearch(t *testing.T) {
if len(r8.Order) != 0 {
t.Fatal("returned wrong serach result")
}
+
+ r9 := (<-store.Post().Search(teamId, userId, "mattermost jersey", false)).Data.(*model.PostList)
+ if len(r9.Order) != 2 {
+ t.Fatal("returned wrong search result")
+ }
}
diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx
index b7cb248db..6b80f6012 100644
--- a/web/react/components/channel_loader.jsx
+++ b/web/react/components/channel_loader.jsx
@@ -8,6 +8,7 @@
var BrowserStore = require('../stores/browser_store.jsx');
var AsyncClient = require('../utils/async_client.jsx');
var SocketStore = require('../stores/socket_store.jsx');
+var ChannelStore = require('../stores/channel_store.jsx');
var Constants = require('../utils/constants.jsx');
module.exports = React.createClass({
@@ -15,7 +16,7 @@ module.exports = React.createClass({
/* Start initial aysnc loads */
AsyncClient.getMe();
- AsyncClient.getPosts(true);
+ AsyncClient.getPosts(true, ChannelStore.getCurrentId(), Constants.POST_CHUNK_SIZE);
AsyncClient.getChannels(true, true);
AsyncClient.getChannelExtraInfo(true);
AsyncClient.findTeams();
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index 327520210..76286eb88 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -31,11 +31,6 @@ module.exports = React.createClass({
post.message = this.state.messageText;
- // if this is a reply, trim off any carets from the beginning of a message
- if (this.state.rootId && post.message[0] === "^") {
- post.message = post.message.replace(/^\^+\s*/g, "");
- }
-
if (post.message.trim().length === 0 && this.state.previews.length === 0) {
return;
}
@@ -73,9 +68,6 @@ module.exports = React.createClass({
post.channel_id = this.state.channel_id;
post.filenames = this.state.previews;
- post.root_id = this.state.rootId;
- post.parent_id = this.state.parentId;
-
client.createPost(post, ChannelStore.getCurrent(),
function(data) {
PostStore.storeDraft(data.channel_id, null);
@@ -92,12 +84,7 @@ module.exports = React.createClass({
}.bind(this),
function(err) {
var state = {}
-
- if (err.message === "Invalid RootId parameter") {
- if ($('#post_deleted').length > 0) $('#post_deleted').modal('show');
- } else {
- state.server_error = err.message;
- }
+ state.server_error = err.message;
state.submitting = false;
this.setState(state);
@@ -106,17 +93,6 @@ module.exports = React.createClass({
}
$(".post-list-holder-by-time").perfectScrollbar('update');
-
- if (this.state.rootId || this.state.parentId) {
- this.setState({rootId: "", parentId: "", caretCount: 0});
-
- // clear the active thread since we've now sent our message
- AppDispatcher.handleViewAction({
- type: ActionTypes.RECEIVED_ACTIVE_THREAD_CHANGED,
- root_id: "",
- parent_id: ""
- });
- }
},
componentDidUpdate: function() {
this.resizePostHolder();
@@ -138,62 +114,6 @@ module.exports = React.createClass({
this.resizePostHolder();
this.setState({messageText: messageText});
- // look to see if the message begins with any carets to indicate that it's a reply
- var replyMatch = messageText.match(/^\^+/g);
- if (replyMatch) {
- // the number of carets indicates how many message threads back we're replying to
- var caretCount = replyMatch[0].length;
-
- // note that if someone else replies to this thread while a user is typing a reply, the message to which they're replying
- // won't change unless they change the number of carets. this is probably the desired behaviour since we don't want the
- // active message thread to change without the user noticing
- if (caretCount != this.state.caretCount) {
- this.setState({caretCount: caretCount});
-
- var posts = PostStore.getCurrentPosts();
-
- var rootId = "";
-
- // find the nth most recent post that isn't a comment on another (ie it has no parent) where n is caretCount
- for (var i = 0; i < posts.order.length; i++) {
- var postId = posts.order[i];
-
- if (posts.posts[postId].parent_id === "") {
- caretCount -= 1;
-
- if (caretCount < 1) {
- rootId = postId;
- break;
- }
- }
- }
-
- // only dispatch an event if something changed
- if (rootId != this.state.rootId) {
- // set the parent id to match the root id so that we're replying to the first post in the thread
- var parentId = rootId;
-
- // alert the post list so that it can display the active thread
- AppDispatcher.handleViewAction({
- type: ActionTypes.RECEIVED_ACTIVE_THREAD_CHANGED,
- root_id: rootId,
- parent_id: parentId
- });
- }
- }
- } else {
- if (this.state.caretCount > 0) {
- this.setState({caretCount: 0});
-
- // clear the active thread since there no longer is one
- AppDispatcher.handleViewAction({
- type: ActionTypes.RECEIVED_ACTIVE_THREAD_CHANGED,
- root_id: "",
- parent_id: ""
- });
- }
- }
-
var draft = PostStore.getCurrentDraft();
if (!draft) {
draft = {}
@@ -256,12 +176,10 @@ module.exports = React.createClass({
},
componentDidMount: function() {
ChannelStore.addChangeListener(this._onChange);
- PostStore.addActiveThreadChangedListener(this._onActiveThreadChanged);
this.resizePostHolder();
},
componentWillUnmount: function() {
ChannelStore.removeChangeListener(this._onChange);
- PostStore.removeActiveThreadChangedListener(this._onActiveThreadChanged);
},
_onChange: function() {
var channel_id = ChannelStore.getCurrentId();
@@ -278,11 +196,6 @@ module.exports = React.createClass({
this.setState({ channel_id: channel_id, messageText: messageText, initialText: messageText, submitting: false, limit_error: null, server_error: null, post_error: null, previews: previews, uploadsInProgress: uploadsInProgress });
}
},
- _onActiveThreadChanged: function(rootId, parentId) {
- // note that we register for our own events and set the state from there so we don't need to manually set
- // our state and dispatch an event each time the active thread changes
- this.setState({"rootId": rootId, "parentId": parentId});
- },
getInitialState: function() {
PostStore.clearDraftUploads();
@@ -293,7 +206,7 @@ module.exports = React.createClass({
previews = draft['previews'];
messageText = draft['message'];
}
- return { channel_id: ChannelStore.getCurrentId(), messageText: messageText, uploadsInProgress: 0, previews: previews, submitting: false, initialText: messageText, caretCount: 0 };
+ return { channel_id: ChannelStore.getCurrentId(), messageText: messageText, uploadsInProgress: 0, previews: previews, submitting: false, initialText: messageText };
},
setUploads: function(val) {
var oldInProgress = this.state.uploadsInProgress
diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx
index 3cd791887..b7ea5734f 100644
--- a/web/react/components/file_attachment.jsx
+++ b/web/react/components/file_attachment.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
var utils = require('../utils/utils.jsx');
+var Constants = require('../utils/constants.jsx');
module.exports = React.createClass({
displayName: "FileAttachment",
@@ -44,6 +45,16 @@ module.exports = React.createClass({
$(imgDiv).removeClass('post__load');
$(imgDiv).addClass('post__image');
+ var width = this.width || $(this).width();
+ var height = this.height || $(this).height();
+
+ if (width < Constants.THUMBNAIL_WIDTH
+ && height < Constants.THUMBNAIL_HEIGHT) {
+ $(imgDiv).addClass('small');
+ } else {
+ $(imgDiv).addClass('normal');
+ }
+
var re1 = new RegExp(' ', 'g');
var re2 = new RegExp('\\(', 'g');
var re3 = new RegExp('\\)', 'g');
diff --git a/web/react/components/member_list.jsx b/web/react/components/member_list.jsx
index a37392f96..69da5cfc3 100644
--- a/web/react/components/member_list.jsx
+++ b/web/react/components/member_list.jsx
@@ -13,7 +13,7 @@ module.exports = React.createClass({
var message = "";
if (members.length === 0)
- message = <span>No users to add or manage.</span>;
+ message = <span>No users to add.</span>;
return (
<div className="member-list-holder">
diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx
index e3586ecde..e72a2d001 100644
--- a/web/react/components/post.jsx
+++ b/web/react/components/post.jsx
@@ -83,7 +83,7 @@ module.exports = React.createClass({
<img className="post-profile-img" src={"/api/v1/users/" + post.user_id + "/image?time=" + timestamp} height="36" width="36" />
</div>
: null }
- <div className={"post__content" + (this.props.isActiveThread ? " active-thread__content" : "")}>
+ <div className="post__content">
<PostHeader ref="header" post={post} sameRoot={this.props.sameRoot} commentCount={commentCount} handleCommentClick={this.handleCommentClick} isLastComment={this.props.isLastComment} />
<PostBody post={post} sameRoot={this.props.sameRoot} parentPost={parentPost} posts={posts} handleCommentClick={this.handleCommentClick} />
<PostInfo ref="info" post={post} sameRoot={this.props.sameRoot} commentCount={commentCount} handleCommentClick={this.handleCommentClick} allowReply="true" />
diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx
index 46f77660d..3f59d5843 100644
--- a/web/react/components/post_list.jsx
+++ b/web/react/components/post_list.jsx
@@ -22,8 +22,7 @@ function getStateFromStores() {
return {
post_list: PostStore.getCurrentPosts(),
- channel: channel,
- activeThreadRootId: ""
+ channel: channel
};
}
@@ -52,7 +51,6 @@ module.exports = React.createClass({
ChannelStore.addChangeListener(this._onChange);
UserStore.addStatusesChangeListener(this._onTimeChange);
SocketStore.addChangeListener(this._onSocketChange);
- PostStore.addActiveThreadChangedListener(this._onActiveThreadChanged);
$(".post-list-holder-by-time").perfectScrollbar();
@@ -133,7 +131,6 @@ module.exports = React.createClass({
ChannelStore.removeChangeListener(this._onChange);
UserStore.removeStatusesChangeListener(this._onTimeChange);
SocketStore.removeChangeListener(this._onSocketChange);
- PostStore.removeActiveThreadChangedListener(this._onActiveThreadChanged);
$('body').off('click.userpopover');
},
resize: function() {
@@ -232,9 +229,6 @@ module.exports = React.createClass({
this.refs[id].forceUpdateInfo();
}
},
- _onActiveThreadChanged: function(rootId, parentId) {
- this.setState({"activeThreadRootId": rootId});
- },
getMorePosts: function(e) {
e.preventDefault();
@@ -429,12 +423,9 @@ module.exports = React.createClass({
// it is the last comment if it is last post in the channel or the next post has a different root post
var isLastComment = utils.isComment(post) && (i === 0 || posts[order[i-1]].root_id != post.root_id);
- // check if this is part of the thread that we're currently replying to
- var isActiveThread = this.state.activeThreadRootId && (post.id === this.state.activeThreadRootId || post.root_id === this.state.activeThreadRootId);
-
var postCtl = (
<Post ref={post.id} sameUser={sameUser} sameRoot={sameRoot} post={post} parentPost={parentPost} key={post.id}
- posts={posts} hideProfilePic={hideProfilePic} isLastComment={isLastComment} isActiveThread={isActiveThread}
+ posts={posts} hideProfilePic={hideProfilePic} isLastComment={isLastComment}
/>
);
diff --git a/web/react/components/signup_team_complete.jsx b/web/react/components/signup_team_complete.jsx
index 83daa3b1f..447a405bd 100644
--- a/web/react/components/signup_team_complete.jsx
+++ b/web/react/components/signup_team_complete.jsx
@@ -496,60 +496,58 @@ SendInivtesPage = React.createClass({
});
UsernamePage = React.createClass({
- submitBack: function (e) {
+ submitBack: function(e) {
e.preventDefault();
- this.props.state.wizard = "send_invites";
+ this.props.state.wizard = 'send_invites';
this.props.updateParent(this.props.state);
},
- submitNext: function (e) {
+ submitNext: function(e) {
e.preventDefault();
var name = this.refs.name.getDOMNode().value.trim();
var username_error = utils.isValidUsername(name);
- if (username_error === "Cannot use a reserved word as a username.") {
- this.setState({name_error: "This username is reserved, please choose a new one." });
+ if (username_error === 'Cannot use a reserved word as a username.') {
+ this.setState({name_error: 'This username is reserved, please choose a new one.'});
return;
} else if (username_error) {
- this.setState({name_error: "Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'." });
+ this.setState({name_error: "Username must begin with a letter, and contain 3 to 15 characters in total, which may be numbers, lowercase letters, or any of the symbols '.', '-', or '_'"});
return;
}
-
- this.props.state.wizard = "password";
+ this.props.state.wizard = 'password';
this.props.state.user.username = name;
this.props.updateParent(this.props.state);
},
getInitialState: function() {
- return { };
+ return {};
},
render: function() {
-
client.track('signup', 'signup_team_06_username');
- var name_error = this.state.name_error ? <label className="control-label">{ this.state.name_error }</label> : null;
+ var name_error = this.state.name_error ? <label className='control-label'>{this.state.name_error}</label> : null;
return (
<div>
<form>
- <img className="signup-team-logo" src="/static/images/logo.png" />
- <h2 className="margin--less">Your username</h2>
- <h5 className="color--light">{"Select a memorable username that makes it easy for " + strings.Team + "mates to identify you:"}</h5>
- <div className="inner__content margin--extra">
- <div className={ name_error ? "form-group has-error" : "form-group" }>
- <div className="row">
- <div className="col-sm-11">
+ <img className='signup-team-logo' src='/static/images/logo.png' />
+ <h2 className='margin--less'>Your username</h2>
+ <h5 className='color--light'>{'Select a memorable username that makes it easy for ' + strings.Team + 'mates to identify you:'}</h5>
+ <div className='inner__content margin--extra'>
+ <div className={name_error ? 'form-group has-error' : 'form-group'}>
+ <div className='row'>
+ <div className='col-sm-11'>
<h5><strong>Choose your username</strong></h5>
- <input autoFocus={true} type="text" ref="name" className="form-control" placeholder="" defaultValue={this.props.state.user.username} maxLength="128" />
- <div className="color--light form__hint">Usernames must begin with a letter and contain 3 to 15 characters made up of lowercase letters, numbers, and the symbols '.', '-' and '_'</div>
+ <input autoFocus={true} type='text' ref='name' className='form-control' placeholder='' defaultValue={this.props.state.user.username} maxLength='128' />
+ <div className='color--light form__hint'>Usernames must begin with a letter and contain 3 to 15 characters made up of lowercase letters, numbers, and the symbols '.', '-' and '_'</div>
</div>
</div>
- { name_error }
+ {name_error}
</div>
</div>
- <button type="submit" className="btn btn-primary margin--extra" onClick={this.submitNext}>Next<i className="glyphicon glyphicon-chevron-right"></i></button>
- <div className="margin--extra">
- <a href="#" onClick={this.submitBack}>Back to previous step</a>
+ <button type='submit' className='btn btn-primary margin--extra' onClick={this.submitNext}>Next<i className='glyphicon glyphicon-chevron-right'></i></button>
+ <div className='margin--extra'>
+ <a href='#' onClick={this.submitBack}>Back to previous step</a>
</div>
</form>
</div>
diff --git a/web/react/components/user_settings.jsx b/web/react/components/user_settings.jsx
index 902989b7b..95d1178d1 100644
--- a/web/react/components/user_settings.jsx
+++ b/web/react/components/user_settings.jsx
@@ -638,8 +638,8 @@ var GeneralTab = React.createClass({
var username = this.state.username.trim();
var username_error = utils.isValidUsername(username);
- if (username_error === "Cannot use a reserved word as a username.") {
- this.setState({client_error: "This username is reserved, please choose a new one." });
+ if (username_error === 'Cannot use a reserved word as a username.') {
+ this.setState({client_error: 'This username is reserved, please choose a new one.' });
return;
} else if (username_error) {
this.setState({client_error: "Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'." });
@@ -647,7 +647,7 @@ var GeneralTab = React.createClass({
}
if (user.username === username) {
- this.setState({client_error: "You must submit a new username"});
+ this.setState({client_error: 'You must submit a new username'});
return;
}
@@ -662,7 +662,7 @@ var GeneralTab = React.createClass({
var nickname = this.state.nickname.trim();
if (user.nickname === nickname) {
- this.setState({client_error: "You must submit a new nickname"})
+ this.setState({client_error: 'You must submit a new nickname'})
return;
}
@@ -678,7 +678,7 @@ var GeneralTab = React.createClass({
var lastName = this.state.last_name.trim();
if (user.first_name === firstName && user.last_name === lastName) {
- this.setState({client_error: "You must submit a new first or last name"})
+ this.setState({client_error: 'You must submit a new first or last name'})
return;
}
@@ -698,7 +698,7 @@ var GeneralTab = React.createClass({
}
if (email === '' || !utils.isEmail(email)) {
- this.setState({ email_error: "Please enter a valid email address" });
+ this.setState({ email_error: 'Please enter a valid email address' });
return;
}
@@ -708,16 +708,16 @@ var GeneralTab = React.createClass({
},
submitUser: function(user) {
client.updateUser(user,
- function(data) {
- this.updateSection("");
+ function() {
+ this.updateSection('');
AsyncClient.getMe();
}.bind(this),
function(err) {
state = this.getInitialState();
- if(err.message) {
+ if (err.message) {
state.server_error = err.message;
} else {
- state.server_error = err
+ state.server_error = err;
}
this.setState(state);
}.bind(this)
@@ -726,22 +726,26 @@ var GeneralTab = React.createClass({
submitPicture: function(e) {
e.preventDefault();
- if (!this.state.picture) return;
+ if (!this.state.picture) {
+ return;
+ }
- if(!this.submitActive) return;
+ if (!this.submitActive) {
+ return;
+ }
var picture = this.state.picture;
- if(picture.type !== "image/jpeg" && picture.type !== "image/png") {
- this.setState({client_error: "Only JPG or PNG images may be used for profile pictures"});
+ if (picture.type !== 'image/jpeg' && picture.type !== 'image/png') {
+ this.setState({client_error: 'Only JPG or PNG images may be used for profile pictures'});
return;
}
- formData = new FormData();
+ var formData = new FormData();
formData.append('image', picture, picture.name);
client.uploadProfileImage(formData,
- function(data) {
+ function() {
this.submitActive = false;
AsyncClient.getMe();
window.location.reload();
@@ -754,39 +758,39 @@ var GeneralTab = React.createClass({
);
},
updateUsername: function(e) {
- this.setState({ username: e.target.value });
+ this.setState({username: e.target.value});
},
updateFirstName: function(e) {
- this.setState({ first_name: e.target.value });
+ this.setState({first_name: e.target.value});
},
updateLastName: function(e) {
- this.setState({ last_name: e.target.value});
+ this.setState({last_name: e.target.value});
},
updateNickname: function(e) {
this.setState({nickname: e.target.value});
},
updateEmail: function(e) {
- this.setState({ email: e.target.value});
+ this.setState({email: e.target.value});
},
updatePicture: function(e) {
if (e.target.files && e.target.files[0]) {
this.setState({ picture: e.target.files[0] });
this.submitActive = true;
- this.setState({client_error:null})
+ this.setState({client_error: null});
} else {
- this.setState({ picture: null });
+ this.setState({picture: null});
}
},
updateSection: function(section) {
- this.setState({client_error:""})
- this.submitActive = false
+ this.setState({client_error:''});
+ this.submitActive = false;
this.props.updateSection(section);
},
handleClose: function() {
- $(this.getDOMNode()).find(".form-control").each(function() {
- this.value = "";
+ $(this.getDOMNode()).find('.form-control').each(function() {
+ this.value = '';
});
this.setState(assign({}, this.getInitialState(), {client_error: null, server_error: null, email_error: null}));
@@ -812,43 +816,45 @@ var GeneralTab = React.createClass({
var nameSection;
var self = this;
+ var inputs = [];
if (this.props.activeSection === 'name') {
- var inputs = [];
-
inputs.push(
- <div className="form-group">
- <label className="col-sm-5 control-label">First Name</label>
- <div className="col-sm-7">
- <input className="form-control" type="text" onChange={this.updateFirstName} value={this.state.first_name}/>
+ <div className='form-group'>
+ <label className='col-sm-5 control-label'>First Name</label>
+ <div className='col-sm-7'>
+ <input className='form-control' type='text' onChange={this.updateFirstName} value={this.state.first_name}/>
</div>
</div>
);
inputs.push(
- <div className="form-group">
- <label className="col-sm-5 control-label">Last Name</label>
- <div className="col-sm-7">
- <input className="form-control" type="text" onChange={this.updateLastName} value={this.state.last_name}/>
+ <div className='form-group'>
+ <label className='col-sm-5 control-label'>Last Name</label>
+ <div className='col-sm-7'>
+ <input className='form-control' type='text' onChange={this.updateLastName} value={this.state.last_name}/>
</div>
</div>
);
nameSection = (
<SettingItemMax
- title="Full Name"
+ title='Full Name'
inputs={inputs}
submit={this.submitName}
server_error={server_error}
client_error={client_error}
- updateSection={function(e){self.updateSection("");e.preventDefault();}}
+ updateSection={function(e) {
+ self.updateSection('');
+ e.preventDefault();
+ }}
/>
);
} else {
- var full_name = "";
+ var full_name = '';
if (user.first_name && user.last_name) {
- full_name = user.first_name + " " + user.last_name;
+ full_name = user.first_name + ' ' + user.last_name;
} else if (user.first_name) {
full_name = user.first_name;
} else if (user.last_name) {
@@ -857,107 +863,119 @@ var GeneralTab = React.createClass({
nameSection = (
<SettingItemMin
- title="Full Name"
+ title='Full Name'
describe={full_name}
- updateSection={function(){self.updateSection("name");}}
+ updateSection={function() {
+ self.updateSection('name');
+ }}
/>
);
}
var nicknameSection;
if (this.props.activeSection === 'nickname') {
- var inputs = [];
inputs.push(
- <div className="form-group">
- <label className="col-sm-5 control-label">{utils.isMobile() ? "": "Nickname"}</label>
- <div className="col-sm-7">
- <input className="form-control" type="text" onChange={this.updateNickname} value={this.state.nickname}/>
+ <div className='form-group'>
+ <label className='col-sm-5 control-label'>{utils.isMobile() ? '' : 'Nickname'}</label>
+ <div className='col-sm-7'>
+ <input className='form-control' type='text' onChange={this.updateNickname} value={this.state.nickname}/>
</div>
</div>
);
nicknameSection = (
<SettingItemMax
- title="Nickname"
+ title='Nickname'
inputs={inputs}
submit={this.submitNickname}
server_error={server_error}
client_error={client_error}
- updateSection={function(e){self.updateSection("");e.preventDefault();}}
+ updateSection={function(e) {
+ self.updateSection('');
+ e.preventDefault();
+ }}
/>
);
} else {
nicknameSection = (
<SettingItemMin
- title="Nickname"
+ title='Nickname'
describe={UserStore.getCurrentUser().nickname}
- updateSection={function(){self.updateSection("nickname");}}
+ updateSection={function() {
+ self.updateSection('nickname');
+ }}
/>
);
}
var usernameSection;
if (this.props.activeSection === 'username') {
- var inputs = [];
-
inputs.push(
- <div className="form-group">
- <label className="col-sm-5 control-label">{utils.isMobile() ? "": "Username"}</label>
- <div className="col-sm-7">
- <input className="form-control" type="text" onChange={this.updateUsername} value={this.state.username}/>
+ <div className='form-group'>
+ <label className='col-sm-5 control-label'>{utils.isMobile() ? '': 'Username'}</label>
+ <div className='col-sm-7'>
+ <input className='form-control' type='text' onChange={this.updateUsername} value={this.state.username}/>
</div>
</div>
);
usernameSection = (
<SettingItemMax
- title="Username"
+ title='Username'
inputs={inputs}
submit={this.submitUsername}
server_error={server_error}
client_error={client_error}
- updateSection={function(e){self.updateSection("");e.preventDefault();}}
+ updateSection={function(e) {
+ self.updateSection('');
+ e.preventDefault();
+ }}
/>
);
} else {
usernameSection = (
<SettingItemMin
- title="Username"
+ title='Username'
describe={UserStore.getCurrentUser().username}
- updateSection={function(){self.updateSection("username");}}
+ updateSection={function() {
+ self.updateSection('username');
+ }}
/>
);
}
var emailSection;
if (this.props.activeSection === 'email') {
- var inputs = [];
-
inputs.push(
- <div className="form-group">
- <label className="col-sm-5 control-label">Primary Email</label>
- <div className="col-sm-7">
- <input className="form-control" type="text" onChange={this.updateEmail} value={this.state.email}/>
+ <div className='form-group'>
+ <label className='col-sm-5 control-label'>Primary Email</label>
+ <div className='col-sm-7'>
+ <input className='form-control' type='text' onChange={this.updateEmail} value={this.state.email}/>
</div>
</div>
);
emailSection = (
<SettingItemMax
- title="Email"
+ title='Email'
inputs={inputs}
submit={this.submitEmail}
server_error={server_error}
client_error={email_error}
- updateSection={function(e){self.updateSection("");e.preventDefault();}}
+ updateSection={function(e) {
+ self.updateSection('');
+ e.preventDefault();
+ }}
/>
);
} else {
emailSection = (
<SettingItemMin
- title="Email"
+ title='Email'
describe={UserStore.getCurrentUser().email}
- updateSection={function(){self.updateSection("email");}}
+ updateSection={function() {
+ self.updateSection('email');
+ }}
/>
);
}
@@ -966,57 +984,60 @@ var GeneralTab = React.createClass({
if (this.props.activeSection === 'picture') {
pictureSection = (
<SettingPicture
- title="Profile Picture"
+ title='Profile Picture'
submit={this.submitPicture}
- src={"/api/v1/users/" + user.id + "/image?time=" + user.last_picture_update}
+ src={'/api/v1/users/' + user.id + '/image?time=' + user.last_picture_update}
server_error={server_error}
client_error={client_error}
- updateSection={function(e){self.updateSection("");e.preventDefault();}}
+ updateSection={function(e) {
+ self.updateSection('');
+ e.preventDefault();
+ }}
picture={this.state.picture}
pictureChange={this.updatePicture}
submitActive={this.submitActive}
/>
);
-
} else {
- var minMessage = "Click Edit to upload an image.";
+ var minMessage = 'Click \'Edit\' to upload an image.';
if (user.last_picture_update) {
- minMessage = "Image last updated " + utils.displayDate(user.last_picture_update)
+ minMessage = 'Image last updated ' + utils.displayDate(user.last_picture_update);
}
pictureSection = (
<SettingItemMin
- title="Profile Picture"
+ title='Profile Picture'
describe={minMessage}
- updateSection={function(){self.updateSection("picture");}}
+ updateSection={function() {
+ self.updateSection('picture');
+ }}
/>
);
}
return (
<div>
- <div className="modal-header">
- <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
- <h4 className="modal-title" ref="title"><i className="modal-back"></i>General Settings</h4>
+ <div className='modal-header'>
+ <button type='button' className='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>&times;</span></button>
+ <h4 className='modal-title' ref='title'><i className='modal-back'></i>General Settings</h4>
</div>
- <div className="user-settings">
- <h3 className="tab-header">General Settings</h3>
- <div className="divider-dark first"/>
+ <div className='user-settings'>
+ <h3 className='tab-header'>General Settings</h3>
+ <div className='divider-dark first'/>
{nameSection}
- <div className="divider-light"/>
+ <div className='divider-light'/>
{usernameSection}
- <div className="divider-light"/>
+ <div className='divider-light'/>
{nicknameSection}
- <div className="divider-light"/>
+ <div className='divider-light'/>
{emailSection}
- <div className="divider-light"/>
+ <div className='divider-light'/>
{pictureSection}
- <div className="divider-dark"/>
+ <div className='divider-dark'/>
</div>
</div>
);
}
});
-
var AppearanceTab = React.createClass({
submitTheme: function(e) {
e.preventDefault();
diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx
index 0745fcdc3..ecf54ede6 100644
--- a/web/react/stores/post_store.jsx
+++ b/web/react/stores/post_store.jsx
@@ -18,7 +18,6 @@ var SEARCH_TERM_CHANGE_EVENT = 'search_term_change';
var SELECTED_POST_CHANGE_EVENT = 'selected_post_change';
var MENTION_DATA_CHANGE_EVENT = 'mention_data_change';
var ADD_MENTION_EVENT = 'add_mention';
-var ACTIVE_THREAD_CHANGED_EVENT = 'active_thread_changed';
var PostStore = assign({}, EventEmitter.prototype, {
@@ -94,18 +93,6 @@ var PostStore = assign({}, EventEmitter.prototype, {
this.removeListener(ADD_MENTION_EVENT, callback);
},
- emitActiveThreadChanged: function(rootId, parentId) {
- this.emit(ACTIVE_THREAD_CHANGED_EVENT, rootId, parentId);
- },
-
- addActiveThreadChangedListener: function(callback) {
- this.on(ACTIVE_THREAD_CHANGED_EVENT, callback);
- },
-
- removeActiveThreadChangedListener: function(callback) {
- this.removeListener(ACTIVE_THREAD_CHANGED_EVENT, callback);
- },
-
getCurrentPosts: function() {
var currentId = ChannelStore.getCurrentId();
@@ -211,10 +198,6 @@ PostStore.dispatchToken = AppDispatcher.register(function(payload) {
case ActionTypes.RECIEVED_ADD_MENTION:
PostStore.emitAddMention(action.id, action.username);
break;
- case ActionTypes.RECEIVED_ACTIVE_THREAD_CHANGED:
- PostStore.emitActiveThreadChanged(action.root_id, action.parent_id);
- break;
-
default:
}
});
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index 00bd83ed1..dc4fc1096 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -272,17 +272,23 @@ module.exports.search = function(terms) {
);
}
-module.exports.getPosts = function(force, id) {
+module.exports.getPosts = function(force, id, maxPosts) {
if (PostStore.getCurrentPosts() == null || force) {
var channelId = id ? id : ChannelStore.getCurrentId();
if (isCallInProgress("getPosts_"+channelId)) return;
var post_list = PostStore.getCurrentPosts();
+
+ if (!maxPosts) { maxPosts = Constants.POST_CHUNK_SIZE * Constants.MAX_POST_CHUNKS };
+
// if we already have more than POST_CHUNK_SIZE posts,
// let's get the amount we have but rounded up to next multiple of POST_CHUNK_SIZE,
- // with a max at 180
- var numPosts = post_list && post_list.order.length > 0 ? Math.min(180, Constants.POST_CHUNK_SIZE * Math.ceil(post_list.order.length / Constants.POST_CHUNK_SIZE)) : Constants.POST_CHUNK_SIZE;
+ // with a max at maxPosts
+ var numPosts = Math.min(maxPosts, Constants.POST_CHUNK_SIZE);
+ if (post_list && post_list.order.length > 0) {
+ numPosts = Math.min(maxPosts, Constants.POST_CHUNK_SIZE * Math.ceil(post_list.order.length / Constants.POST_CHUNK_SIZE));
+ }
if (channelId != null) {
callTracker["getPosts_"+channelId] = utils.getTimestamp();
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 77ce19530..2b0976afd 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -18,7 +18,6 @@ module.exports = {
RECIEVED_POST_SELECTED: null,
RECIEVED_MENTION_DATA: null,
RECIEVED_ADD_MENTION: null,
- RECEIVED_ACTIVE_THREAD_CHANGED: null,
RECIEVED_PROFILES: null,
RECIEVED_ME: null,
@@ -52,9 +51,12 @@ module.exports = {
MAX_DISPLAY_FILES: 5,
MAX_UPLOAD_FILES: 5,
MAX_FILE_SIZE: 50000000, // 50 MB
+ THUMBNAIL_WIDTH: 128,
+ THUMBNAIL_HEIGHT: 100,
DEFAULT_CHANNEL: 'town-square',
OFFTOPIC_CHANNEL: 'off-topic',
POST_CHUNK_SIZE: 60,
+ MAX_POST_CHUNKS: 3,
RESERVED_TEAM_NAMES: [
"www",
"web",
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 09240bf06..a759cc579 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -750,7 +750,7 @@ module.exports.switchChannel = function(channel, teammate_name) {
AsyncClient.getChannels(true, true, true);
AsyncClient.getChannelExtraInfo(true);
- AsyncClient.getPosts(true, channel.id);
+ AsyncClient.getPosts(true, channel.id, Constants.POST_CHUNK_SIZE);
$('.inner__wrap').removeClass('move--right');
$('.sidebar--left').removeClass('move--right');
diff --git a/web/sass-files/sass/partials/_files.scss b/web/sass-files/sass/partials/_files.scss
index ea7548267..ddc5e98bb 100644
--- a/web/sass-files/sass/partials/_files.scss
+++ b/web/sass-files/sass/partials/_files.scss
@@ -129,7 +129,12 @@
height: 100%;
background-color: #FFF;
background-repeat: no-repeat;
- background-position: top left;
+ &.small {
+ background-position: center;
+ }
+ &.normal {
+ background-position: top left;
+ }
}
.post-image__thumbnail {
width: 50%;
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index df565d763..98b17120d 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -319,12 +319,6 @@ body.ios {
max-width: 100%;
@include legacy-pie-clearfix;
}
- &.active-thread__content {
- // this still needs a final style applied to it
- & .post-body {
- font-weight: bold;
- }
- }
}
.post-image__columns {
@include legacy-pie-clearfix;