summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/command.go112
-rw-r--r--api/command_test.go37
-rw-r--r--api/templates/signup_team_body.html2
-rw-r--r--model/command.go2
-rw-r--r--store/sql_post_store.go14
-rw-r--r--store/sql_post_store_test.go17
-rw-r--r--web/react/components/command_list.jsx6
-rw-r--r--web/react/components/create_comment.jsx7
-rw-r--r--web/react/components/create_post.jsx7
-rw-r--r--web/react/components/edit_post_modal.jsx6
-rw-r--r--web/react/components/post_info.jsx17
-rw-r--r--web/react/components/post_list.jsx42
-rw-r--r--web/react/components/setting_item_max.jsx2
-rw-r--r--web/react/components/sidebar_header.jsx2
-rw-r--r--web/react/components/user_settings_general.jsx237
-rw-r--r--web/react/components/user_settings_notifications.jsx16
-rw-r--r--web/react/stores/channel_store.jsx1
-rw-r--r--web/react/utils/utils.jsx95
-rw-r--r--web/sass-files/sass/partials/_command-box.scss16
-rw-r--r--web/sass-files/sass/partials/_settings.scss5
-rw-r--r--web/templates/head.html2
21 files changed, 425 insertions, 220 deletions
diff --git a/api/command.go b/api/command.go
index f051bd42e..749cbf790 100644
--- a/api/command.go
+++ b/api/command.go
@@ -4,15 +4,15 @@
package api
import (
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
l4g "code.google.com/p/log4go"
"github.com/gorilla/mux"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
- "net/http"
- "reflect"
- "runtime"
- "strconv"
- "strings"
)
type commandHandler func(c *Context, command *model.Command) bool
@@ -24,6 +24,8 @@ var commands = []commandHandler{
echoCommand,
}
+var echoSem chan bool
+
func InitCommand(r *mux.Router) {
l4g.Debug("Initializing command api routes")
r.Handle("/command", ApiUserRequired(command)).Methods("POST")
@@ -41,7 +43,6 @@ func command(c *Context, w http.ResponseWriter, r *http.Request) {
}
checkCommand(c, command)
-
if c.Err != nil {
return
} else {
@@ -56,8 +57,6 @@ func checkCommand(c *Context, command *model.Command) bool {
return false
}
- tchan := Srv.Store.Team().Get(c.Session.TeamId)
-
if len(command.ChannelId) > 0 {
cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, command.ChannelId, c.Session.UserId)
@@ -66,24 +65,9 @@ func checkCommand(c *Context, command *model.Command) bool {
}
}
- allowValet := false
- if tResult := <-tchan; tResult.Err != nil {
- c.Err = model.NewAppError("checkCommand", "Could not find the team for this session, team_id="+c.Session.TeamId, "")
- return false
- } else {
- allowValet = tResult.Data.(*model.Team).AllowValet
- }
-
- ec := runtime.FuncForPC(reflect.ValueOf(echoCommand).Pointer()).Name()
-
for _, v := range commands {
- if !allowValet && ec == runtime.FuncForPC(reflect.ValueOf(v).Pointer()).Name() {
- continue
- }
- if v(c, command) {
- return true
- } else if c.Err != nil {
+ if v(c, command) || c.Err != nil {
return true
}
}
@@ -112,55 +96,65 @@ func logoutCommand(c *Context, command *model.Command) bool {
}
func echoCommand(c *Context, command *model.Command) bool {
-
cmd := "/echo"
+ maxThreads := 100
- if strings.Index(command.Command, cmd) == 0 {
- parts := strings.SplitN(command.Command, " ", 3)
-
- channelName := ""
- if len(parts) >= 2 {
- channelName = parts[1]
+ if !command.Suggest && strings.Index(command.Command, cmd) == 0 {
+ parameters := strings.SplitN(command.Command, " ", 2)
+ if len(parameters) != 2 || len(parameters[1]) == 0 {
+ return false
}
-
- message := ""
- if len(parts) >= 3 {
- message = parts[2]
+ message := strings.Trim(parameters[1], " ")
+ delay := 0
+ if endMsg := strings.LastIndex(message, "\""); string(message[0]) == "\"" && endMsg > 1 {
+ if checkDelay, err := strconv.Atoi(strings.Trim(message[endMsg:], " \"")); err == nil {
+ delay = checkDelay
+ }
+ message = message[1:endMsg]
+ } else if strings.Index(message, " ") > -1 {
+ delayIdx := strings.LastIndex(message, " ")
+ delayStr := strings.Trim(message[delayIdx:], " ")
+
+ if checkDelay, err := strconv.Atoi(delayStr); err == nil {
+ delay = checkDelay
+ message = message[:delayIdx]
+ }
}
- if result := <-Srv.Store.Channel().GetChannels(c.Session.TeamId, c.Session.UserId); result.Err != nil {
- c.Err = result.Err
+ if delay > 10000 {
+ c.Err = model.NewAppError("echoCommand", "Delays must be under 10000 seconds", "")
return false
- } else {
- channels := result.Data.(*model.ChannelList)
+ }
- for _, v := range channels.Channels {
- if v.Type == model.CHANNEL_DIRECT {
- continue
- }
+ if echoSem == nil {
+ // We want one additional thread allowed so we never reach channel lockup
+ echoSem = make(chan bool, maxThreads+1)
+ }
- if v.Name == channelName && !command.Suggest {
- post := &model.Post{}
- post.ChannelId = v.Id
- post.Message = message
+ if len(echoSem) >= maxThreads {
+ c.Err = model.NewAppError("echoCommand", "High volume of echo request, cannot process request", "")
+ return false
+ }
- if _, err := CreateValetPost(c, post); err != nil {
- c.Err = err
- return false
- }
+ echoSem <- true
+ go func() {
+ defer func() { <-echoSem }()
+ post := &model.Post{}
+ post.ChannelId = command.ChannelId
+ post.Message = message
- command.Response = model.RESP_EXECUTED
- return true
- }
+ time.Sleep(time.Duration(delay) * time.Second)
- if len(channelName) == 0 || (strings.Index(v.Name, channelName) == 0 && len(parts) < 3) {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd + " " + v.Name, Description: "Echo a message using Valet in a channel"})
- }
+ if _, err := CreatePost(c, post, false); err != nil {
+ l4g.Error("Unable to create /echo post, err=%v", err)
}
- }
+ }()
+
+ command.Response = model.RESP_EXECUTED
+ return true
} else if strings.Index(cmd, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: "Echo a message using Valet in a channel"})
+ command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: "Echo back text from your account, /echo \"message\" [delay in seoncds]"})
}
return false
diff --git a/api/command_test.go b/api/command_test.go
index a58ef9be5..fe52dd41b 100644
--- a/api/command_test.go
+++ b/api/command_test.go
@@ -4,9 +4,10 @@
package api
import (
+ "testing"
+
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
- "testing"
)
func TestSuggestRootCommands(t *testing.T) {
@@ -50,6 +51,12 @@ func TestSuggestRootCommands(t *testing.T) {
if rs3.Suggestions[0].Suggestion != "/join" {
t.Fatal("should have join cmd")
}
+
+ rs4 := Client.Must(Client.Command("", "/ech", true)).Data.(*model.Command)
+
+ if rs4.Suggestions[0].Suggestion != "/echo" {
+ t.Fatal("should have echo cmd")
+ }
}
func TestLogoutCommands(t *testing.T) {
@@ -145,3 +152,31 @@ func TestJoinCommands(t *testing.T) {
t.Fatal("didn't join channel")
}
}
+
+func TestEchoCommand(t *testing.T) {
+ Setup()
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user1.Id))
+
+ Client.LoginByEmail(team.Name, user1.Email, "pwd")
+
+ channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
+
+ echoTestString := "/echo test"
+
+ r1 := Client.Must(Client.Command(channel1.Id, echoTestString, false)).Data.(*model.Command)
+ if r1.Response != model.RESP_EXECUTED {
+ t.Fatal("Echo command failed to execute")
+ }
+
+ p1 := Client.Must(Client.GetPosts(channel1.Id, 0, 2, "")).Data.(*model.PostList)
+ if len(p1.Order) != 1 {
+ t.Fatal("Echo command failed to send")
+ }
+}
diff --git a/api/templates/signup_team_body.html b/api/templates/signup_team_body.html
index 71df0b9c8..b49cf5f36 100644
--- a/api/templates/signup_team_body.html
+++ b/api/templates/signup_team_body.html
@@ -21,7 +21,7 @@
<p style="margin: 20px 0 25px">
<a href="{{.Props.Link}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Set up your team</a>
</p>
- {{ .SiteName }} is free for an unlimited time, for unlimited users.<br>You'll get more out of {{ .SiteName }} when your team is in constant communication--let's get them on board.<br></p>
+ {{ .SiteName }} is one place for all your team communication, searchable and available anywhere.<br>You'll get more out of {{ .SiteName }} when your team is in constant communication--let's get them on board.<br></p>
<p>
Learn more by <a href="{{.Props.TourUrl}}" style="text-decoration: none; color:#2389D7;">taking a tour</a>
</p>
diff --git a/model/command.go b/model/command.go
index 23573205e..83243cc98 100644
--- a/model/command.go
+++ b/model/command.go
@@ -14,7 +14,7 @@ const (
type Command struct {
Command string `json:"command"`
- Response string `json:"reponse"`
+ Response string `json:"response"`
GotoLocation string `json:"goto_location"`
ChannelId string `json:"channel_id"`
Suggest bool `json:"-"`
diff --git a/store/sql_post_store.go b/store/sql_post_store.go
index cd668b13c..297d60397 100644
--- a/store/sql_post_store.go
+++ b/store/sql_post_store.go
@@ -5,9 +5,11 @@ package store
import (
"fmt"
+ "regexp"
+ "strings"
+
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
- "strings"
)
type SqlPostStore struct {
@@ -358,7 +360,7 @@ func (s SqlPostStore) getParentsPosts(channelId string, offset int, limit int) S
var posts []*model.Post
_, err := s.GetReplica().Select(&posts,
- `SELECT
+ `SELECT
q2.*
FROM
Posts q2
@@ -366,7 +368,7 @@ func (s SqlPostStore) getParentsPosts(channelId string, offset int, limit int) S
(SELECT DISTINCT
q3.RootId
FROM
- (SELECT
+ (SELECT
RootId
FROM
Posts
@@ -417,6 +419,12 @@ func (s SqlPostStore) Search(teamId string, userId string, terms string, isHasht
var posts []*model.Post
if utils.Cfg.SqlSettings.DriverName == "postgres" {
+
+ // Parse text for wildcards
+ if wildcard, err := regexp.Compile("\\*($| )"); err == nil {
+ terms = wildcard.ReplaceAllLiteralString(terms, ":* ")
+ }
+
searchQuery := fmt.Sprintf(`SELECT
*
FROM
diff --git a/store/sql_post_store_test.go b/store/sql_post_store_test.go
index 4564e2deb..d48dea51c 100644
--- a/store/sql_post_store_test.go
+++ b/store/sql_post_store_test.go
@@ -4,11 +4,11 @@
package store
import (
- "github.com/mattermost/platform/model"
- "github.com/mattermost/platform/utils"
"strings"
"testing"
"time"
+
+ "github.com/mattermost/platform/model"
)
func TestPostStoreSave(t *testing.T) {
@@ -547,11 +547,9 @@ func TestPostStoreSearch(t *testing.T) {
t.Fatal("returned wrong serach result")
}
- if utils.Cfg.SqlSettings.DriverName == "mysql" {
- r5 := (<-store.Post().Search(teamId, userId, "matter*", false)).Data.(*model.PostList)
- if len(r5.Order) != 1 && r5.Order[0] != o1.Id {
- t.Fatal("returned wrong serach result")
- }
+ r5 := (<-store.Post().Search(teamId, userId, "matter*", false)).Data.(*model.PostList)
+ if len(r5.Order) != 1 && r5.Order[0] != o1.Id {
+ t.Fatal("returned wrong serach result")
}
r6 := (<-store.Post().Search(teamId, userId, "#hashtag", true)).Data.(*model.PostList)
@@ -573,4 +571,9 @@ func TestPostStoreSearch(t *testing.T) {
if len(r9.Order) != 2 {
t.Fatal("returned wrong search result")
}
+
+ r10 := (<-store.Post().Search(teamId, userId, "matter* jer*", false)).Data.(*model.PostList)
+ if len(r10.Order) != 2 {
+ t.Fatal("returned wrong search result")
+ }
}
diff --git a/web/react/components/command_list.jsx b/web/react/components/command_list.jsx
index 5efe98dc6..27264ff6e 100644
--- a/web/react/components/command_list.jsx
+++ b/web/react/components/command_list.jsx
@@ -48,15 +48,15 @@ module.exports = React.createClass({
if (this.state.suggestions[i].suggestion != this.state.cmd) {
suggestions.push(
<div key={i} className="command-name" onClick={this.handleClick.bind(this, i)}>
- <div className="pull-left"><strong>{ this.state.suggestions[i].suggestion }</strong></div>
- <div className="command-desc pull-right">{ this.state.suggestions[i].description }</div>
+ <div className="command__title"><strong>{ this.state.suggestions[i].suggestion }</strong></div>
+ <div className="command__desc">{ this.state.suggestions[i].description }</div>
</div>
);
}
}
return (
- <div ref="mentionlist" className="command-box" style={{height:(this.state.suggestions.length*37)+2}}>
+ <div ref="mentionlist" className="command-box" style={{height:(this.state.suggestions.length*56)+2}}>
{ suggestions }
</div>
);
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index f6e34fda9..c2b7e222f 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -104,17 +104,14 @@ module.exports = React.createClass({
this.lastTime = t;
}
},
- handleUserInput: function(message) {
- var messageText = utils.truncateText(message);
- var newPostError = utils.checkMessageLengthError(messageText, this.state.postError, 'Comment length cannot exceed ' + Constants.MAX_POST_LEN + ' characters');
-
+ handleUserInput: function(messageText) {
var draft = PostStore.getCommentDraft(this.props.rootId);
draft.message = messageText;
PostStore.storeCommentDraft(this.props.rootId, draft);
$('.post-right__scroll').scrollTop($('.post-right__scroll')[0].scrollHeight);
$('.post-right__scroll').perfectScrollbar('update');
- this.setState({messageText: messageText, postError: newPostError});
+ this.setState({messageText: messageText});
},
handleUploadStart: function(clientIds, channelId) {
var draft = PostStore.getCommentDraft(this.props.rootId);
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index 73210c855..b9142223f 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -130,12 +130,9 @@ module.exports = React.createClass({
this.lastTime = t;
}
},
- handleUserInput: function(message) {
- var messageText = utils.truncateText(message);
- var newPostError = utils.checkMessageLengthError(messageText, this.state.postError, 'Message length cannot exceed ' + Constants.MAX_POST_LEN + ' characters');
-
+ handleUserInput: function(messageText) {
this.resizePostHolder();
- this.setState({messageText: messageText, postError: newPostError});
+ this.setState({messageText: messageText});
var draft = PostStore.getCurrentDraft();
draft['message'] = messageText;
diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx
index df692e1bb..1c5a1ed5e 100644
--- a/web/react/components/edit_post_modal.jsx
+++ b/web/react/components/edit_post_modal.jsx
@@ -38,10 +38,8 @@ module.exports = React.createClass({
$("#edit_post").modal('hide');
$(this.state.refocusId).focus();
},
- handleEditInput: function(editText) {
- var editMessage = utils.truncateText(editText);
- var newError = utils.checkMessageLengthError(editMessage, this.state.error, 'New message length cannot exceed ' + Constants.MAX_POST_LEN + ' characters');
- this.setState({editText: editMessage, error: newError});
+ handleEditInput: function(editMessage) {
+ this.setState({editText: editMessage});
},
handleEditKeyPress: function(e) {
if (e.which == 13 && !e.shiftKey && !e.altKey) {
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index 73e897f62..c80b287a3 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -43,13 +43,16 @@ export default class PostInfo extends React.Component {
if (isOwner) {
dropdownContents.push(
- <li role='presentation'>
+ <li
+ key='editPost'
+ role='presentation'
+ >
<a
href='#'
role='menuitem'
data-toggle='modal'
data-target='#edit_post'
- data-refoucsid="#post_textbox"
+ data-refoucsid='#post_textbox'
data-title={type}
data-message={post.message}
data-postid={post.id}
@@ -64,7 +67,10 @@ export default class PostInfo extends React.Component {
if (isOwner || isAdmin) {
dropdownContents.push(
- <li role='presentation'>
+ <li
+ key='deletePost'
+ role='presentation'
+ >
<a
href='#'
role='menuitem'
@@ -83,7 +89,10 @@ export default class PostInfo extends React.Component {
if (this.props.allowReply === 'true') {
dropdownContents.push(
- <li role='presentation'>
+ <li
+ key='replyLink'
+ role='presentation'
+ >
<a
className='reply-link theme'
href='#'
diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx
index 865a22dbd..c1e6e490d 100644
--- a/web/react/components/post_list.jsx
+++ b/web/react/components/post_list.jsx
@@ -31,9 +31,11 @@ export default class PostList extends React.Component {
this.onSocketChange = this.onSocketChange.bind(this);
this.createChannelIntroMessage = this.createChannelIntroMessage.bind(this);
this.loadMorePosts = this.loadMorePosts.bind(this);
+ this.loadFirstPosts = this.loadFirstPosts.bind(this);
this.state = this.getStateFromStores();
this.state.numToDisplay = Constants.POST_CHUNK_SIZE;
+ this.state.isFirstLoadComplete = false;
}
getStateFromStores() {
var channel = ChannelStore.getCurrent();
@@ -157,7 +159,10 @@ export default class PostList extends React.Component {
});
this.scrollToBottom();
- setTimeout(this.scrollToBottom, 100);
+
+ if (this.state.channel.id != null) {
+ this.loadFirstPosts(this.state.channel.id);
+ }
}
componentDidUpdate(prevProps, prevState) {
$('.post-list__content div .post').removeClass('post--last');
@@ -229,9 +234,26 @@ export default class PostList extends React.Component {
postHolder.removeClass('hide-scroll');
}
}
+ loadFirstPosts(id) {
+ Client.getPosts(
+ id,
+ PostStore.getLatestUpdate(id),
+ function success() {
+ this.setState({isFirstLoadComplete: true});
+ }.bind(this),
+ function fail() {
+ this.setState({isFirstLoadComplete: true});
+ }.bind(this)
+ );
+ }
onChange() {
var newState = this.getStateFromStores();
+ // Special case where the channel wasn't yet set in componentDidMount
+ if (!this.state.isFirstLoadComplete && this.state.channel.id == null && newState.channel.id != null) {
+ this.loadFirstPosts(newState.channel.id);
+ }
+
if (!utils.areStatesEqual(newState, this.state)) {
if (this.state.channel.id !== newState.channel.id) {
PostStore.clearUnseenDeletedPosts(this.state.channel.id);
@@ -379,6 +401,14 @@ export default class PostList extends React.Component {
>
<i className='fa fa-pencil'></i>Set a description
</a>
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#channel_invite'
+ >
+ <i className='fa fa-user-plus'></i>Invite others to this channel
+ </a>
</div>
);
}
@@ -511,9 +541,15 @@ export default class PostList extends React.Component {
if (post.user_id !== userId && post.create_at > this.state.lastViewed && !renderedLastViewed) {
renderedLastViewed = true;
+
+ // Temporary fix to solve ie10/11 rendering issue
+ let newSeparatorId = '';
+ if (!utils.isBrowserIE()) {
+ newSeparatorId = 'new_message';
+ }
postCtls.push(
<div
- id='new_message'
+ id={newSeparatorId}
key='unviewed'
className='new-separator'
>
@@ -605,7 +641,7 @@ export default class PostList extends React.Component {
}
var postCtls = [];
- if (posts) {
+ if (posts && this.state.isFirstLoadComplete) {
postCtls = this.createPosts(posts, order);
} else {
postCtls.push(<LoadingScreen position='absolute' />);
diff --git a/web/react/components/setting_item_max.jsx b/web/react/components/setting_item_max.jsx
index 1599041b0..b978cdb0c 100644
--- a/web/react/components/setting_item_max.jsx
+++ b/web/react/components/setting_item_max.jsx
@@ -5,6 +5,7 @@ module.exports = React.createClass({
render: function() {
var clientError = this.props.client_error ? <div className='form-group'><label className='col-sm-12 has-error'>{ this.props.client_error }</label></div> : null;
var server_error = this.props.server_error ? <div className='form-group'><label className='col-sm-12 has-error'>{ this.props.server_error }</label></div> : null;
+ var extraInfo = this.props.extraInfo ? this.props.extraInfo : null;
var inputs = this.props.inputs;
@@ -15,6 +16,7 @@ module.exports = React.createClass({
<ul className="setting-list">
<li className="setting-list-item">
{inputs}
+ {extraInfo}
</li>
<li className="setting-list-item">
<hr />
diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx
index d5d16816f..af65b7e1d 100644
--- a/web/react/components/sidebar_header.jsx
+++ b/web/react/components/sidebar_header.jsx
@@ -85,7 +85,7 @@ var NavbarDropdown = React.createClass({
}
});
}
- teams.push(<li key='newTeam_li'><a key='newTeam_a' href={utils.getWindowLocationOrigin() + '/signup_team' }>Create a New Team</a></li>);
+ teams.push(<li key='newTeam_li'><a key='newTeam_a' target="_blank" href={utils.getWindowLocationOrigin() + '/signup_team' }>Create a New Team</a></li>);
return (
<ul className='nav navbar-nav navbar-right'>
diff --git a/web/react/components/user_settings_general.jsx b/web/react/components/user_settings_general.jsx
index fed11fbe9..ddd2fb607 100644
--- a/web/react/components/user_settings_general.jsx
+++ b/web/react/components/user_settings_general.jsx
@@ -11,10 +11,32 @@ var AsyncClient = require('../utils/async_client.jsx');
var utils = require('../utils/utils.jsx');
var assign = require('object-assign');
-module.exports = React.createClass({
- displayName: 'GeneralTab',
- submitActive: false,
- submitUsername: function(e) {
+export default class UserSettingsGeneralTab extends React.Component {
+ constructor(props) {
+ super(props);
+ this.submitActive = false;
+
+ this.submitUsername = this.submitUsername.bind(this);
+ this.submitNickname = this.submitNickname.bind(this);
+ this.submitName = this.submitName.bind(this);
+ this.submitEmail = this.submitEmail.bind(this);
+ this.submitUser = this.submitUser.bind(this);
+ this.submitPicture = this.submitPicture.bind(this);
+
+ this.updateUsername = this.updateUsername.bind(this);
+ this.updateFirstName = this.updateFirstName.bind(this);
+ this.updateLastName = this.updateLastName.bind(this);
+ this.updateNickname = this.updateNickname.bind(this);
+ this.updateEmail = this.updateEmail.bind(this);
+ this.updatePicture = this.updatePicture.bind(this);
+ this.updateSection = this.updateSection.bind(this);
+
+ this.handleClose = this.handleClose.bind(this);
+ this.setupInitialState = this.setupInitialState.bind(this);
+
+ this.state = this.setupInitialState(props);
+ }
+ submitUsername(e) {
e.preventDefault();
var user = this.props.user;
@@ -37,8 +59,8 @@ module.exports = React.createClass({
user.username = username;
this.submitUser(user);
- },
- submitNickname: function(e) {
+ }
+ submitNickname(e) {
e.preventDefault();
var user = UserStore.getCurrentUser();
@@ -52,8 +74,8 @@ module.exports = React.createClass({
user.nickname = nickname;
this.submitUser(user);
- },
- submitName: function(e) {
+ }
+ submitName(e) {
e.preventDefault();
var user = UserStore.getCurrentUser();
@@ -69,8 +91,8 @@ module.exports = React.createClass({
user.last_name = lastName;
this.submitUser(user);
- },
- submitEmail: function(e) {
+ }
+ submitEmail(e) {
e.preventDefault();
var user = UserStore.getCurrentUser();
@@ -88,15 +110,15 @@ module.exports = React.createClass({
user.email = email;
this.submitUser(user);
- },
- submitUser: function(user) {
+ }
+ submitUser(user) {
client.updateUser(user,
- function() {
+ function updateSuccess() {
this.updateSection('');
AsyncClient.getMe();
}.bind(this),
- function(err) {
- var state = this.getInitialState();
+ function updateFailure(err) {
+ var state = this.setupInitialState(this.props);
if (err.message) {
state.serverError = err.message;
} else {
@@ -105,8 +127,8 @@ module.exports = React.createClass({
this.setState(state);
}.bind(this)
);
- },
- submitPicture: function(e) {
+ }
+ submitPicture(e) {
e.preventDefault();
if (!this.state.picture) {
@@ -129,34 +151,34 @@ module.exports = React.createClass({
this.setState({loadingPicture: true});
client.uploadProfileImage(formData,
- function() {
+ function imageUploadSuccess() {
this.submitActive = false;
AsyncClient.getMe();
window.location.reload();
}.bind(this),
- function(err) {
- var state = this.getInitialState();
+ function imageUploadFailure(err) {
+ var state = this.setupInitialState(this.props);
state.serverError = err;
this.setState(state);
}.bind(this)
);
- },
- updateUsername: function(e) {
+ }
+ updateUsername(e) {
this.setState({username: e.target.value});
- },
- updateFirstName: function(e) {
+ }
+ updateFirstName(e) {
this.setState({firstName: e.target.value});
- },
- updateLastName: function(e) {
+ }
+ updateLastName(e) {
this.setState({lastName: e.target.value});
- },
- updateNickname: function(e) {
+ }
+ updateNickname(e) {
this.setState({nickname: e.target.value});
- },
- updateEmail: function(e) {
+ }
+ updateEmail(e) {
this.setState({email: e.target.value});
- },
- updatePicture: function(e) {
+ }
+ updatePicture(e) {
if (e.target.files && e.target.files[0]) {
this.setState({picture: e.target.files[0]});
@@ -165,34 +187,33 @@ module.exports = React.createClass({
} else {
this.setState({picture: null});
}
- },
- updateSection: function(section) {
- this.setState(assign({}, this.getInitialState(), {clientError: ''}));
+ }
+ updateSection(section) {
+ this.setState(assign({}, this.setupInitialState(this.props), {clientError: '', serverError: '', emailError: ''}));
this.submitActive = false;
this.props.updateSection(section);
- },
- handleClose: function() {
- $(this.getDOMNode()).find('.form-control').each(function() {
+ }
+ handleClose() {
+ $(this.getDOMNode()).find('.form-control').each(function clearForms() {
this.value = '';
});
- this.setState(assign({}, this.getInitialState(), {clientError: null, serverError: null, emailError: null}));
+ this.setState(assign({}, this.setupInitialState(this.props), {clientError: null, serverError: null, emailError: null}));
this.props.updateSection('');
- },
- componentDidMount: function() {
+ }
+ componentDidMount() {
$('#user_settings').on('hidden.bs.modal', this.handleClose);
- },
- componentWillUnmount: function() {
+ }
+ componentWillUnmount() {
$('#user_settings').off('hidden.bs.modal', this.handleClose);
- },
- getInitialState: function() {
- var user = this.props.user;
+ }
+ setupInitialState(props) {
+ var user = props.user;
var emailEnabled = !ConfigStore.getSettingAsBoolean('ByPassEmail', false);
-
return {username: user.username, firstName: user.first_name, lastName: user.last_name, nickname: user.nickname,
- email: user.email, picture: null, loadingPicture: false, emailEnabled: emailEnabled};
- },
- render: function() {
+ email: user.email, picture: null, loadingPicture: false, emailEnabled: emailEnabled};
+ }
+ render() {
var user = this.props.user;
var clientError = null;
@@ -214,19 +235,35 @@ module.exports = React.createClass({
if (this.props.activeSection === 'name') {
inputs.push(
- <div className='form-group'>
+ <div
+ key='firstNameSetting'
+ 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.firstName}/>
+ <input
+ className='form-control'
+ type='text'
+ onChange={this.updateFirstName}
+ value={this.state.firstName}
+ />
</div>
</div>
);
inputs.push(
- <div className='form-group'>
+ <div
+ key='lastNameSetting'
+ 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.lastName}/>
+ <input
+ className='form-control'
+ type='text'
+ onChange={this.updateLastName}
+ value={this.state.lastName}
+ />
</div>
</div>
);
@@ -238,7 +275,7 @@ module.exports = React.createClass({
submit={this.submitName}
server_error={serverError}
client_error={clientError}
- updateSection={function(e) {
+ updateSection={function clearSection(e) {
self.updateSection('');
e.preventDefault();
}}
@@ -259,7 +296,7 @@ module.exports = React.createClass({
<SettingItemMin
title='Full Name'
describe={fullName}
- updateSection={function() {
+ updateSection={function updateNameSection() {
self.updateSection('name');
}}
/>
@@ -268,11 +305,24 @@ module.exports = React.createClass({
var nicknameSection;
if (this.props.activeSection === 'nickname') {
+ let nicknameLabel = 'Nickname';
+ if (utils.isMobile()) {
+ nicknameLabel = '';
+ }
+
inputs.push(
- <div className='form-group'>
- <label className='col-sm-5 control-label'>{utils.isMobile() ? '' : 'Nickname'}</label>
+ <div
+ key='nicknameSetting'
+ className='form-group'
+ >
+ <label className='col-sm-5 control-label'>{nicknameLabel}</label>
<div className='col-sm-7'>
- <input className='form-control' type='text' onChange={this.updateNickname} value={this.state.nickname}/>
+ <input
+ className='form-control'
+ type='text'
+ onChange={this.updateNickname}
+ value={this.state.nickname}
+ />
</div>
</div>
);
@@ -284,7 +334,7 @@ module.exports = React.createClass({
submit={this.submitNickname}
server_error={serverError}
client_error={clientError}
- updateSection={function(e) {
+ updateSection={function clearSection(e) {
self.updateSection('');
e.preventDefault();
}}
@@ -295,7 +345,7 @@ module.exports = React.createClass({
<SettingItemMin
title='Nickname'
describe={UserStore.getCurrentUser().nickname}
- updateSection={function() {
+ updateSection={function updateNicknameSection() {
self.updateSection('nickname');
}}
/>
@@ -304,11 +354,24 @@ module.exports = React.createClass({
var usernameSection;
if (this.props.activeSection === 'username') {
+ let usernameLabel = 'Username';
+ if (utils.isMobile()) {
+ usernameLabel = '';
+ }
+
inputs.push(
- <div className='form-group'>
- <label className='col-sm-5 control-label'>{utils.isMobile() ? '' : 'Username'}</label>
+ <div
+ key='usernameSetting'
+ className='form-group'
+ >
+ <label className='col-sm-5 control-label'>{usernameLabel}</label>
<div className='col-sm-7'>
- <input className='form-control' type='text' onChange={this.updateUsername} value={this.state.username}/>
+ <input
+ className='form-control'
+ type='text'
+ onChange={this.updateUsername}
+ value={this.state.username}
+ />
</div>
</div>
);
@@ -320,7 +383,7 @@ module.exports = React.createClass({
submit={this.submitUsername}
server_error={serverError}
client_error={clientError}
- updateSection={function(e) {
+ updateSection={function clearSection(e) {
self.updateSection('');
e.preventDefault();
}}
@@ -331,7 +394,7 @@ module.exports = React.createClass({
<SettingItemMin
title='Username'
describe={UserStore.getCurrentUser().username}
- updateSection={function() {
+ updateSection={function updateUsernameSection() {
self.updateSection('username');
}}
/>
@@ -346,11 +409,16 @@ module.exports = React.createClass({
}
inputs.push(
- <div>
+ <div key='emailSetting'>
<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}/>
+ <input
+ className='form-control'
+ type='text'
+ onChange={this.updateEmail}
+ value={this.state.email}
+ />
</div>
</div>
{helpText}
@@ -364,7 +432,7 @@ module.exports = React.createClass({
submit={this.submitEmail}
server_error={serverError}
client_error={emailError}
- updateSection={function(e) {
+ updateSection={function clearSection(e) {
self.updateSection('');
e.preventDefault();
}}
@@ -375,7 +443,7 @@ module.exports = React.createClass({
<SettingItemMin
title='Email'
describe={UserStore.getCurrentUser().email}
- updateSection={function() {
+ updateSection={function updateEmailSection() {
self.updateSection('email');
}}
/>
@@ -391,7 +459,7 @@ module.exports = React.createClass({
src={'/api/v1/users/' + user.id + '/image?time=' + user.last_picture_update}
server_error={serverError}
client_error={clientError}
- updateSection={function(e) {
+ updateSection={function clearSection(e) {
self.updateSection('');
e.preventDefault();
}}
@@ -410,7 +478,7 @@ module.exports = React.createClass({
<SettingItemMin
title='Profile Picture'
describe={minMessage}
- updateSection={function() {
+ updateSection={function updatePictureSection() {
self.updateSection('picture');
}}
/>
@@ -419,8 +487,21 @@ module.exports = React.createClass({
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>
+ <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>
@@ -439,4 +520,10 @@ module.exports = React.createClass({
</div>
);
}
-});
+}
+
+UserSettingsGeneralTab.propTypes = {
+ user: React.PropTypes.object,
+ updateSection: React.PropTypes.func,
+ activeSection: React.PropTypes.string
+};
diff --git a/web/react/components/user_settings_notifications.jsx b/web/react/components/user_settings_notifications.jsx
index b89f72987..ba0bda78e 100644
--- a/web/react/components/user_settings_notifications.jsx
+++ b/web/react/components/user_settings_notifications.jsx
@@ -1,6 +1,7 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
+var ChannelStore = require('../stores/channel_store.jsx');
var UserStore = require('../stores/user_store.jsx');
var SettingItemMin = require('./setting_item_min.jsx');
var SettingItemMax = require('./setting_item_max.jsx');
@@ -67,7 +68,11 @@ function getNotificationsStateFromStores() {
}
}
- return {notifyLevel: desktop, enableEmail: email, soundNeeded: soundNeeded, enableSound: sound, usernameKey: usernameKey, mentionKey: mentionKey, customKeys: customKeys, customKeysChecked: customKeys.length > 0, firstNameKey: firstNameKey, allKey: allKey, channelKey: channelKey};
+ var curChannel = ChannelStore.getCurrent().display_name;
+
+ return {notifyLevel: desktop, enableEmail: email, soundNeeded: soundNeeded, enableSound: sound,
+ usernameKey: usernameKey, mentionKey: mentionKey, customKeys: customKeys, customKeysChecked: customKeys.length > 0,
+ firstNameKey: firstNameKey, allKey: allKey, channelKey: channelKey, curChannel: curChannel};
}
export default class NotificationsTab extends React.Component {
@@ -141,10 +146,12 @@ export default class NotificationsTab extends React.Component {
}
componentDidMount() {
UserStore.addChangeListener(this.onListenerChange);
+ ChannelStore.addChangeListener(this.onListenerChange);
$('#user_settings').on('hidden.bs.modal', this.handleClose);
}
componentWillUnmount() {
UserStore.removeChangeListener(this.onListenerChange);
+ ChannelStore.removeChangeListener(this.onListenerChange);
$('#user_settings').off('hidden.bs.modal', this.handleClose);
this.props.updateSection('');
}
@@ -265,6 +272,12 @@ export default class NotificationsTab extends React.Component {
e.preventDefault();
};
+ let extraInfo = (
+ <div className='setting-list__hint'>
+ These settings will override the global notification settings for the <b>{this.state.curChannel}</b> channel
+ </div>
+ )
+
desktopSection = (
<SettingItemMax
title='Send desktop notifications'
@@ -272,6 +285,7 @@ export default class NotificationsTab extends React.Component {
submit={this.handleSubmit}
server_error={serverError}
updateSection={handleUpdateDesktopSection}
+ extraInfo={extraInfo}
/>
);
} else {
diff --git a/web/react/stores/channel_store.jsx b/web/react/stores/channel_store.jsx
index f7c23841c..678d50bbd 100644
--- a/web/react/stores/channel_store.jsx
+++ b/web/react/stores/channel_store.jsx
@@ -270,4 +270,5 @@ ChannelStore.dispatchToken = AppDispatcher.register(function(payload) {
}
});
+ChannelStore.setMaxListeners(11);
module.exports = ChannelStore;
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 34a0d55da..a1dc72ae2 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -260,10 +260,40 @@ module.exports.escapeRegExp = function(string) {
return string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1');
};
+function handleYoutubeTime(link) {
+ var timeRegex = /[\\?&]t=([0-9hms]+)/;
+
+ var time = link.trim().match(timeRegex);
+ if (!time || !time[1]) {
+ return '';
+ }
+
+ var hours = time[1].match(/([0-9]+)h/);
+ var minutes = time[1].match(/([0-9]+)m/);
+ var seconds = time[1].match(/([0-9]+)s/);
+
+ var ticks = 0;
+
+ if (hours && hours[1]) {
+ ticks += parseInt(hours[1], 10) * 3600;
+ }
+
+ if (minutes && minutes[1]) {
+ ticks += parseInt(minutes[1], 10) * 60;
+ }
+
+ if (seconds && seconds[1]) {
+ ticks += parseInt(seconds[1], 10);
+ }
+
+ return '&start=' + ticks.toString();
+}
+
function getYoutubeEmbed(link) {
var regex = /.*(?:youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|watch\?(?:[a-zA-Z-_]+=[a-zA-Z0-9-_]+&)+v=)([^#\&\?]*).*/;
var youtubeId = link.trim().match(regex)[1];
+ var time = handleYoutubeTime(link);
function onClick(e) {
var div = $(e.target).closest('.video-thumbnail__container')[0];
@@ -271,7 +301,8 @@ function getYoutubeEmbed(link) {
iframe.setAttribute('src',
'https://www.youtube.com/embed/' +
div.id +
- '?autoplay=1&autohide=1&border=0&wmode=opaque&fs=1&enablejsapi=1');
+ '?autoplay=1&autohide=1&border=0&wmode=opaque&fs=1&enablejsapi=1' +
+ time);
iframe.setAttribute('width', '480px');
iframe.setAttribute('height', '360px');
iframe.setAttribute('type', 'text/html');
@@ -286,6 +317,7 @@ function getYoutubeEmbed(link) {
return;
}
var metadata = data.items[0].snippet;
+ $('.video-type.' + youtubeId).html("Youtube - ")
$('.video-uploader.' + youtubeId).html(metadata.channelTitle);
$('.video-title.' + youtubeId).find('a').html(metadata.title);
$('.post-list-holder-by-time').scrollTop($('.post-list-holder-by-time')[0].scrollHeight);
@@ -303,9 +335,11 @@ function getYoutubeEmbed(link) {
return (
<div className='post-comment'>
- <h4 className='video-type'>YouTube</h4>
+ <h4>
+ <span className={'video-type ' + youtubeId}>YouTube</span>
+ <span className={'video-title ' + youtubeId}><a href={link}></a></span>
+ </h4>
<h4 className={'video-uploader ' + youtubeId}></h4>
- <h4 className={'video-title ' + youtubeId}><a href={link}></a></h4>
<div className='video-div embed-responsive-item' id={youtubeId} onClick={onClick}>
<div className='embed-responsive embed-responsive-4by3 video-div__placeholder'>
<div id={youtubeId} className='video-thumbnail__container'>
@@ -456,9 +490,21 @@ module.exports.textToJsx = function(text, options) {
var mentionRegex = /^(?:@)([a-z0-9_]+)$/gi; // looks loop invariant but a weird JS bug needs it to be redefined here
var explicitMention = mentionRegex.exec(trimWord);
- if ((trimWord.toLowerCase().indexOf(searchTerm) > -1 || word.toLowerCase().indexOf(searchTerm) > -1) && searchTerm != '') {
-
- highlightSearchClass = ' search-highlight';
+ if (searchTerm !== '') {
+ let searchWords = searchTerm.split(' ');
+ for (let idx in searchWords) {
+ let searchWord = searchWords[idx];
+ if (searchWord === word.toLowerCase() || searchWord === trimWord.toLowerCase()) {
+ highlightSearchClass = ' search-highlight';
+ break;
+ } else if (searchWord.charAt(searchWord.length - 1) === '*') {
+ let searchWordPrefix = searchWord.slice(0,-1);
+ if (trimWord.toLowerCase().indexOf(searchWordPrefix) > -1 || word.toLowerCase().indexOf(searchWordPrefix) > -1) {
+ highlightSearchClass = ' search-highlight';
+ break;
+ }
+ }
+ }
}
if (explicitMention &&
@@ -1001,43 +1047,6 @@ module.exports.isBrowserEdge = function() {
return window.naviagtor && navigator.userAgent && navigator.userAgent.toLowerCase().indexOf('edge') > -1;
};
-// Gets text length consistent with maxlength property of textarea html tag
-module.exports.getLengthOfTextInTextarea = function(messageText) {
- // Need to get length with carriage returns counting as two characters to match textbox maxlength behavior
- // unless ie10/ie11/edge which already do
-
- var len = messageText.length;
- if (!module.exports.isBrowserIE() && !module.exports.isBrowserEdge()) {
- len = messageText.replace(/\r(?!\n)|\n(?!\r)/g, '--').length;
- }
-
- return len;
-};
-
-module.exports.checkMessageLengthError = function(message, currentError, newError) {
- var updatedError = currentError;
- var len = module.exports.getLengthOfTextInTextarea(message);
-
- if (!currentError && len >= Constants.MAX_POST_LEN) {
- updatedError = newError;
- } else if (currentError === newError && len < Constants.MAX_POST_LEN) {
- updatedError = '';
- }
-
- return updatedError;
-};
-
-// Necessary due to issues with textarea max length and pasting newlines
-module.exports.truncateText = function(message) {
- var lengthDifference = module.exports.getLengthOfTextInTextarea(message) - message.length;
-
- if (lengthDifference > 0) {
- return message.substring(0, Constants.MAX_POST_LEN - lengthDifference);
- }
-
- return message.substring(0, Constants.MAX_POST_LEN);
-};
-
// Used to get the id of the other user from a DM channel
module.exports.getUserIdFromChannelName = function(channel) {
var ids = channel.name.split('__');
diff --git a/web/sass-files/sass/partials/_command-box.scss b/web/sass-files/sass/partials/_command-box.scss
index 44eb9b8df..565296fae 100644
--- a/web/sass-files/sass/partials/_command-box.scss
+++ b/web/sass-files/sass/partials/_command-box.scss
@@ -4,20 +4,30 @@
width: 100%;
border: $border-gray;
bottom: 38px;
+ overflow: auto;
@extend %popover-box-shadow;
+ .sidebar--right & {
+ bottom: 100px;
+ }
}
.command-name {
position: relative;
width: 100%;
background-color: #fff;
- height: 37px;
- line-height: 37px;
- padding: 2px 10px 2px 5px;
+ line-height: 24px;
+ padding: 5px 10px 8px;
z-index: 101;
+ font-size: 0.95em;
+ border-bottom: 1px solid #ddd;
&:hover {
background-color: #e8eaed;
}
+ .command__desc {
+ margin-left: 5px;
+ color: #999;
+ line-height: normal;
+ }
}
.command-desc {
diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss
index 99a7eb7bc..8d743dfe2 100644
--- a/web/sass-files/sass/partials/_settings.scss
+++ b/web/sass-files/sass/partials/_settings.scss
@@ -94,6 +94,11 @@
list-style-type:none;
}
+ .setting-list__hint {
+ color: #555;
+ margin-top: 20px;
+ }
+
.mentions-input {
margin-top: 10px;
}
diff --git a/web/templates/head.html b/web/templates/head.html
index c27597f82..6b6b7f09d 100644
--- a/web/templates/head.html
+++ b/web/templates/head.html
@@ -7,7 +7,7 @@
<!-- iOS add to homescreen -->
<meta name="apple-mobile-web-app-capable" content="yes" />
- <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
+ <meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="{{ .Title }}">
<meta name="application-name" content="{{ .Title }}">