diff options
Diffstat (limited to 'web/react')
25 files changed, 572 insertions, 311 deletions
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/file_attachment.jsx b/web/react/components/file_attachment.jsx index ab550d500..45e6c5e28 100644 --- a/web/react/components/file_attachment.jsx +++ b/web/react/components/file_attachment.jsx @@ -10,7 +10,7 @@ module.exports = React.createClass({ canSetState: false, propTypes: { // a list of file pathes displayed by the parent FileAttachmentList - filenames: React.PropTypes.arrayOf(React.PropTypes.string).isRequired, + filename: React.PropTypes.string.isRequired, // the index of this attachment preview in the parent FileAttachmentList index: React.PropTypes.number.isRequired, // the identifier of the modal dialog used to preview files @@ -22,9 +22,17 @@ module.exports = React.createClass({ return {fileSize: -1}; }, componentDidMount: function() { + this.loadFiles(); + }, + componentDidUpdate: function(prevProps) { + if (this.props.filename !== prevProps.filename) { + this.loadFiles(); + } + }, + loadFiles: function() { this.canSetState = true; - var filename = this.props.filenames[this.props.index]; + var filename = this.props.filename; if (filename) { var fileInfo = utils.splitFileLocation(filename); @@ -71,6 +79,10 @@ module.exports = React.createClass({ this.canSetState = false; }, shouldComponentUpdate: function(nextProps, nextState) { + if (!utils.areStatesEqual(nextProps, this.props)) { + return true; + } + // the only time this object should update is when it receives an updated file size which we can usually handle without re-rendering if (nextState.fileSize != this.state.fileSize) { if (this.refs.fileSize) { @@ -87,8 +99,7 @@ module.exports = React.createClass({ } }, render: function() { - var filenames = this.props.filenames; - var filename = filenames[this.props.index]; + var filename = this.props.filename; var fileInfo = utils.splitFileLocation(filename); var type = utils.getFileType(fileInfo.ext); diff --git a/web/react/components/file_attachment_list.jsx b/web/react/components/file_attachment_list.jsx index b92442957..df4424d03 100644 --- a/web/react/components/file_attachment_list.jsx +++ b/web/react/components/file_attachment_list.jsx @@ -26,7 +26,7 @@ module.exports = React.createClass({ var postFiles = []; for (var i = 0; i < filenames.length && i < Constants.MAX_DISPLAY_FILES; i++) { - postFiles.push(<FileAttachment key={i} filenames={filenames} index={i} modalId={modalId} handleImageClick={this.handleImageClick} />); + postFiles.push(<FileAttachment key={i} filename={filenames[i]} index={i} modalId={modalId} handleImageClick={this.handleImageClick} />); } return ( diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index 678a2ff87..0f3aa42db 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -10,7 +10,9 @@ var Constants = require('../utils/constants.jsx'); export default class Login extends React.Component { constructor(props) { super(props); + this.handleSubmit = this.handleSubmit.bind(this); + this.state = {}; } handleSubmit(e) { @@ -96,19 +98,16 @@ export default class Login extends React.Component { var authServices = JSON.parse(this.props.authServices); var loginMessage = []; - if (authServices.indexOf(Constants.GITLAB_SERVICE) >= 0) { + if (authServices.indexOf(Constants.GITLAB_SERVICE) !== -1) { loginMessage.push( - <div className='form-group form-group--small'> - <span><a href={'/' + teamName + '/login/gitlab'}>{'Log in with GitLab'}</a></span> - </div> - ); - } - if (authServices.indexOf(Constants.GOOGLE_SERVICE) >= 0) { - loginMessage.push( - <div className='form-group form-group--small'> - <span><a href={'/' + teamName + '/login/google'}>{'Log in with Google'}</a></span> - </div> - ); + <a + className='btn btn-custom-login gitlab' + href={'/' + teamName + '/login/gitlab'} + > + <span className='icon' /> + <span>with GitLab</span> + </a> + ); } var errorClass = ''; @@ -116,15 +115,10 @@ export default class Login extends React.Component { errorClass = ' has-error'; } - return ( - <div className='signup-team__container'> - <h5 className='margin--less'>Sign in to:</h5> - <h2 className='signup-team__name'>{teamDisplayName}</h2> - <h2 className='signup-team__subdomain'>on {config.SiteName}</h2> - <form onSubmit={this.handleSubmit}> - <div className={'form-group' + errorClass}> - {serverError} - </div> + var emailSignup; + if (authServices.indexOf(Constants.EMAIL_SERVICE) !== -1) { + emailSignup = ( + <div> <div className={'form-group' + errorClass}> <input autoFocus={focusEmail} @@ -154,13 +148,43 @@ export default class Login extends React.Component { Sign in </button> </div> + </div> + ); + } + + var forgotPassword; + if (loginMessage.length > 0 && emailSignup) { + loginMessage = ( + <div> {loginMessage} + <div className='or__container'> + <span>or</span> + </div> + </div> + ); + + forgotPassword = ( + <div className='form-group'> + <a href={'/' + teamName + '/reset_password'}>I forgot my password</a> + </div> + ); + } + + return ( + <div className='signup-team__container'> + <h5 className='margin--less'>Sign in to:</h5> + <h2 className='signup-team__name'>{teamDisplayName}</h2> + <h2 className='signup-team__subdomain'>on {config.SiteName}</h2> + <form onSubmit={this.handleSubmit}> + <div className={'form-group' + errorClass}> + {serverError} + </div> + {loginMessage} + {emailSignup} <div className='form-group margin--extra form-group--small'> <span><a href='/find_team'>{'Find other ' + strings.TeamPlural}</a></span> </div> - <div className='form-group'> - <a href={'/' + teamName + '/reset_password'}>I forgot my password</a> - </div> + {forgotPassword} <div className='margin--extra'> <span>{'Want to create your own ' + strings.Team + '? '} <a 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/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx index d221ca840..615bc4ef2 100644 --- a/web/react/components/sidebar_right_menu.jsx +++ b/web/react/components/sidebar_right_menu.jsx @@ -15,7 +15,6 @@ module.exports = React.createClass({ var inviteLink = ''; var teamSettingsLink = ''; var manageLink = ''; - var renameLink = ''; var currentUser = UserStore.getCurrentUser(); var isAdmin = false; @@ -48,11 +47,6 @@ module.exports = React.createClass({ <a href='#' data-toggle='modal' data-target='#team_members'><i className='glyphicon glyphicon-wrench'></i>Manage Team</a> </li> ); - renameLink = ( - <li> - <a href='#' data-toggle='modal' data-target='#rename_team_link'><i className='glyphicon glyphicon-pencil'></i>Rename</a> - </li> - ); } var siteName = ''; @@ -77,7 +71,6 @@ module.exports = React.createClass({ {inviteLink} {teamLink} {manageLink} - {renameLink} <li><a href='#' onClick={this.handleLogoutClick}><i className='glyphicon glyphicon-log-out'></i>Logout</a></li> <li className='divider'></li> <li><a target='_blank' href='/static/help/configure_links.html'><i className='glyphicon glyphicon-question-sign'></i>Help</a></li> diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx index edd48e0b9..13640b1e5 100644 --- a/web/react/components/signup_team.jsx +++ b/web/react/components/signup_team.jsx @@ -1,69 +1,49 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. - -var utils = require('../utils/utils.jsx'); -var client = require('../utils/client.jsx'); - -module.exports = React.createClass({ - handleSubmit: function(e) { - e.preventDefault(); - var team = {}; - var state = { server_error: "" }; - - team.email = this.refs.email.getDOMNode().value.trim().toLowerCase(); - if (!team.email || !utils.isEmail(team.email)) { - state.email_error = "Please enter a valid email address"; - state.inValid = true; +var ChoosePage = require('./team_signup_choose_auth.jsx'); +var EmailSignUpPage = require('./team_signup_with_email.jsx'); +var SSOSignupPage = require('./team_signup_with_sso.jsx'); +var Constants = require('../utils/constants.jsx'); + +export default class TeamSignUp extends React.Component { + constructor(props) { + super(props); + + this.updatePage = this.updatePage.bind(this); + + if (props.services.length === 1) { + if (props.services[0] === Constants.EMAIL_SERVICE) { + this.state = {page: 'email', service: ''}; + } else { + this.state = {page: 'service', service: props.services[0]}; + } + } else { + this.state = {page: 'choose', service: ''}; } - else { - state.email_error = ""; - } - - if (state.inValid) { - this.setState(state); - return; + } + updatePage(page, service) { + this.setState({page: page, service: service}); + } + render() { + if (this.state.page === 'email') { + return <EmailSignUpPage />; + } else if (this.state.page === 'service' && this.state.service !== '') { + return <SSOSignupPage service={this.state.service} />; + } else { + return ( + <ChoosePage + services={this.props.services} + updatePage={this.updatePage} + /> + ); } - - client.signupTeam(team.email, - function(data) { - if (data["follow_link"]) { - window.location.href = data["follow_link"]; - } - else { - window.location.href = "/signup_team_confirm/?email=" + encodeURIComponent(team.email); - } - }.bind(this), - function(err) { - state.server_error = err.message; - this.setState(state); - }.bind(this) - ); - }, - getInitialState: function() { - return { }; - }, - render: function() { - - var email_error = this.state.email_error ? <label className='control-label'>{ this.state.email_error }</label> : null; - var server_error = this.state.server_error ? <div className={ "form-group has-error" }><label className='control-label'>{ this.state.server_error }</label></div> : null; - - return ( - <form role="form" onSubmit={this.handleSubmit}> - <div className={ email_error ? "form-group has-error" : "form-group" }> - <input autoFocus={true} type="email" ref="email" className="form-control" placeholder="Email Address" maxLength="128" /> - { email_error } - </div> - { server_error } - <div className="form-group"> - <button className="btn btn-md btn-primary" type="submit">Sign up</button> - </div> - <div className="form-group margin--extra-2x"> - <span><a href="/find_team">{"Find my " + strings.Team}</a></span> - </div> - </form> - ); } -}); - - +} + +TeamSignUp.defaultProps = { + services: [] +}; +TeamSignUp.propTypes = { + services: React.PropTypes.array +}; diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index 0393e0413..2080cc191 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -162,16 +162,35 @@ module.exports = React.createClass({ ); } - if (authServices.indexOf(Constants.GOOGLE_SERVICE) >= 0) { - signupMessage.push( - <a className='btn btn-custom-login google' href={'/' + this.props.teamName + '/signup/google' + window.location.search}> - <span className='icon' /> - <span>with Google</span> - </a> - ); + var emailSignup; + if (authServices.indexOf(Constants.EMAIL_SERVICE) !== -1) { + emailSignup = ( + <div> + <div className='inner__content'> + {email} + {yourEmailIs} + <div className='margin--extra'> + <h5><strong>Choose your username</strong></h5> + <div className={nameDivStyle}> + <input type='text' ref='name' className='form-control' placeholder='' maxLength='128' /> + {nameError} + <p className='form__hint'>Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'</p> + </div> + </div> + <div className='margin--extra'> + <h5><strong>Choose your password</strong></h5> + <div className={passwordDivStyle}> + <input type='password' ref='password' className='form-control' placeholder='' maxLength='128' /> + {passwordError} + </div> + </div> + </div> + <p className='margin--extra'><button type='submit' onClick={this.handleSubmit} className='btn-primary btn'>Create Account</button></p> + </div> + ); } - if (signupMessage.length > 0) { + if (signupMessage.length > 0 && emailSignup) { signupMessage = ( <div> {signupMessage} @@ -196,26 +215,7 @@ module.exports = React.createClass({ <h2 className='signup-team__subdomain'>on {config.SiteName}</h2> <h4 className='color--light'>Let's create your account</h4> {signupMessage} - <div className='inner__content'> - {email} - {yourEmailIs} - <div className='margin--extra'> - <h5><strong>Choose your username</strong></h5> - <div className={nameDivStyle}> - <input type='text' ref='name' className='form-control' placeholder='' maxLength='128' /> - {nameError} - <p className='form__hint'>Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'</p> - </div> - </div> - <div className='margin--extra'> - <h5><strong>Choose your password</strong></h5> - <div className={passwordDivStyle}> - <input type='password' ref='password' className='form-control' placeholder='' maxLength='128' /> - {passwordError} - </div> - </div> - </div> - <p className='margin--extra'><button type='submit' onClick={this.handleSubmit} className='btn-primary btn'>Create Account</button></p> + {emailSignup} {serverError} {termsDisclaimer} </form> diff --git a/web/react/components/signup_user_oauth.jsx b/web/react/components/signup_user_oauth.jsx deleted file mode 100644 index 8b2800bde..000000000 --- a/web/react/components/signup_user_oauth.jsx +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. -// See License.txt for license information. - - -var utils = require('../utils/utils.jsx'); -var client = require('../utils/client.jsx'); -var UserStore = require('../stores/user_store.jsx'); -var BrowserStore = require('../stores/browser_store.jsx'); - -module.exports = React.createClass({ - handleSubmit: function(e) { - e.preventDefault(); - - if (!this.state.user.username) { - this.setState({name_error: "This field is required", email_error: "", password_error: "", server_error: ""}); - return; - } - - var username_error = utils.isValidUsername(this.state.user.username); - if (username_error === "Cannot use a reserved word as a username.") { - this.setState({name_error: "This username is reserved, please choose a new one.", email_error: "", password_error: "", server_error: ""}); - 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 '_'.", email_error: "", password_error: "", server_error: ""}); - return; - } - - this.setState({name_error: "", server_error: ""}); - - this.state.user.allow_marketing = this.refs.email_service.getDOMNode().checked; - - var user = this.state.user; - client.createUser(user, "", "", - function(data) { - client.track('signup', 'signup_user_oauth_02'); - UserStore.setCurrentUser(data); - UserStore.setLastEmail(data.email); - - window.location.href = '/' + this.props.teamName + '/login/' + user.auth_service + '?login_hint=' + user.email; - }.bind(this), - function(err) { - this.state.server_error = err.message; - this.setState(this.state); - }.bind(this) - ); - }, - handleChange: function() { - var user = this.state.user; - user.username = this.refs.name.getDOMNode().value; - this.setState({ user: user }); - }, - getInitialState: function() { - var user = JSON.parse(this.props.user); - return { user: user }; - }, - render: function() { - - client.track('signup', 'signup_user_oauth_01'); - - var name_error = this.state.name_error ? <label className='control-label'>{ this.state.name_error }</label> : null; - var server_error = this.state.server_error ? <div className={ "form-group has-error" }><label className='control-label'>{ this.state.server_error }</label></div> : null; - - var yourEmailIs = this.state.user.email == "" ? "" : <span>Your email address is <b>{ this.state.user.email }.</b></span>; - - return ( - <div> - <img className="signup-team-logo" src="/static/images/logo.png" /> - <h4>Welcome to { config.SiteName }</h4> - <p>{"To continue signing up with " + this.state.user.auth_service + ", please register a username."}</p> - <p>Your username can be made of lowercase letters and numbers.</p> - <label className="control-label">Username</label> - <div className={ name_error ? "form-group has-error" : "form-group" }> - <input type="text" ref="name" className="form-control" placeholder="" maxLength="128" value={this.state.user.username} onChange={this.handleChange} /> - { name_error } - </div> - <p>{"Pick something " + strings.Team + "mates will recognize. Your username is how you will appear to others."}</p> - <p>{ yourEmailIs } You’ll use this address to sign in to {config.SiteName}.</p> - <div className="checkbox"><label><input type="checkbox" ref="email_service" /> It's ok to send me occassional email with updates about the {config.SiteName} service. </label></div> - <p><button onClick={this.handleSubmit} className="btn-primary btn">Create Account</button></p> - { server_error } - <p>By proceeding to create your account and use { config.SiteName }, you agree to our <a href={ config.TermsLink }>Terms of Service</a> and <a href={ config.PrivacyLink }>Privacy Policy</a>. If you do not agree, you cannot use {config.SiteName}.</p> - </div> - ); - } -}); - - diff --git a/web/react/components/team_signup_choose_auth.jsx b/web/react/components/team_signup_choose_auth.jsx new file mode 100644 index 000000000..92ade5d24 --- /dev/null +++ b/web/react/components/team_signup_choose_auth.jsx @@ -0,0 +1,70 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var Constants = require('../utils/constants.jsx'); + +export default class ChooseAuthPage extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + render() { + var buttons = []; + if (this.props.services.indexOf(Constants.GITLAB_SERVICE) !== -1) { + buttons.push( + <a + className='btn btn-custom-login gitlab btn-full' + href='#' + onClick={ + function clickGit(e) { + e.preventDefault(); + this.props.updatePage('service', Constants.GITLAB_SERVICE); + }.bind(this) + } + > + <span className='icon' /> + <span>Create new {strings.Team} with GitLab Account</span> + </a> + ); + } + + if (this.props.services.indexOf(Constants.EMAIL_SERVICE) !== -1) { + buttons.push( + <a + className='btn btn-custom-login email btn-full' + href='#' + onClick={ + function clickEmail(e) { + e.preventDefault(); + this.props.updatePage('email', ''); + }.bind(this) + } + > + <span className='fa fa-envelope' /> + <span>Create new {strings.Team} with email address</span> + </a> + ); + } + + if (buttons.length === 0) { + buttons = <span>No sign-up methods configured, please contact your system administrator.</span>; + } + + return ( + <div> + {buttons} + <div className='form-group margin--extra-2x'> + <span><a href='/find_team'>{'Find my ' + strings.Team}</a></span> + </div> + </div> + ); + } +} + +ChooseAuthPage.defaultProps = { + services: [] +}; +ChooseAuthPage.propTypes = { + services: React.PropTypes.array, + updatePage: React.PropTypes.func +}; diff --git a/web/react/components/team_signup_password_page.jsx b/web/react/components/team_signup_password_page.jsx index e4f35f100..bbe82a5c2 100644 --- a/web/react/components/team_signup_password_page.jsx +++ b/web/react/components/team_signup_password_page.jsx @@ -31,31 +31,35 @@ module.exports = React.createClass({ teamSignup.user.allow_marketing = true; delete teamSignup.wizard; - // var ctl = this; - client.createTeamFromSignup(teamSignup, function success() { client.track('signup', 'signup_team_08_complete'); var props = this.props; - $('#sign-up-button').button('reset'); - props.state.wizard = 'finished'; - props.updateParent(props.state, true); + + client.loginByEmail(teamSignup.team.name, teamSignup.team.email, teamSignup.user.password, + function(data) { + UserStore.setLastEmail(teamSignup.team.email); + UserStore.setCurrentUser(teamSignup.user); + if (this.props.hash > 0) { + BrowserStore.setGlobalItem(this.props.hash, JSON.stringify({wizard: 'finished'})); + } - window.location.href = utils.getWindowLocationOrigin() + '/' + props.state.team.name + '/login?email=' + encodeURIComponent(teamSignup.team.email); + $('#sign-up-button').button('reset'); + props.state.wizard = 'finished'; + props.updateParent(props.state, true); - // client.loginByEmail(teamSignup.team.domain, teamSignup.team.email, teamSignup.user.password, - // function(data) { - // TeamStore.setLastName(teamSignup.team.domain); - // UserStore.setLastEmail(teamSignup.team.email); - // UserStore.setCurrentUser(data); - // window.location.href = '/channels/town-square'; - // }.bind(ctl), - // function(err) { - // this.setState({nameError: err.message}); - // }.bind(ctl) - // ); + window.location.href = '/'; + }.bind(this), + function(err) { + if (err.message === 'Login failed because email address has not been verified') { + window.location.href = '/verify_email?email=' + encodeURIComponent(teamSignup.team.email) + '&teamname=' + encodeURIComponent(teamSignup.team.name); + } else { + this.setState({serverError: err.message}); + } + }.bind(this) + ); }.bind(this), function error(err) { this.setState({serverError: err.message}); diff --git a/web/react/components/team_signup_with_email.jsx b/web/react/components/team_signup_with_email.jsx new file mode 100644 index 000000000..c7204880f --- /dev/null +++ b/web/react/components/team_signup_with_email.jsx @@ -0,0 +1,82 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var utils = require('../utils/utils.jsx'); +var client = require('../utils/client.jsx'); + +export default class EmailSignUpPage extends React.Component { + constructor() { + super(); + + this.handleSubmit = this.handleSubmit.bind(this); + + this.state = {}; + } + handleSubmit(e) { + e.preventDefault(); + var team = {}; + var state = {serverError: ''}; + + team.email = this.refs.email.getDOMNode().value.trim().toLowerCase(); + if (!team.email || !utils.isEmail(team.email)) { + state.emailError = 'Please enter a valid email address'; + state.inValid = true; + } else { + state.emailError = ''; + } + + if (state.inValid) { + this.setState(state); + return; + } + + client.signupTeam(team.email, + function success(data) { + if (data.follow_link) { + window.location.href = data.follow_link; + } else { + window.location.href = '/signup_team_confirm/?email=' + encodeURIComponent(team.email); + } + }, + function fail(err) { + state.serverError = err.message; + this.setState(state); + }.bind(this) + ); + } + render() { + return ( + <form + role='form' + onSubmit={this.handleSubmit} + > + <div className='form-group'> + <input + autoFocus={true} + type='email' + ref='email' + className='form-control' + placeholder='Email Address' + maxLength='128' + /> + </div> + <div className='form-group'> + <button + className='btn btn-md btn-primary' + type='submit' + > + Sign up + </button> + </div> + <div className='form-group margin--extra-2x'> + <span><a href='/find_team'>{'Find my ' + strings.Team}</a></span> + </div> + </form> + ); + } +} + +EmailSignUpPage.defaultProps = { +}; +EmailSignUpPage.propTypes = { +}; diff --git a/web/react/components/team_signup_with_sso.jsx b/web/react/components/team_signup_with_sso.jsx new file mode 100644 index 000000000..6cb62efc7 --- /dev/null +++ b/web/react/components/team_signup_with_sso.jsx @@ -0,0 +1,125 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var utils = require('../utils/utils.jsx'); +var client = require('../utils/client.jsx'); +var Constants = require('../utils/constants.jsx'); + +export default class SSOSignUpPage extends React.Component { + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + this.nameChange = this.nameChange.bind(this); + + this.state = {name: ''}; + } + handleSubmit(e) { + e.preventDefault(); + var team = {}; + var state = this.state; + state.nameError = null; + state.serverError = null; + + team.display_name = this.state.name; + + if (team.display_name.length <= 3) { + return; + } + + if (!team.display_name) { + state.nameError = 'Please enter a team name'; + this.setState(state); + return; + } + + team.name = utils.cleanUpUrlable(team.display_name); + team.type = 'O'; + + client.createTeamWithSSO(team, + this.props.service, + function success(data) { + if (data.follow_link) { + window.location.href = data.follow_link; + } else { + window.location.href = '/'; + } + }, + function fail(err) { + state.serverError = err.message; + this.setState(state); + }.bind(this) + ); + } + nameChange() { + this.setState({name: this.refs.teamname.getDOMNode().value.trim()}); + } + render() { + var nameError = null; + var nameDivClass = 'form-group'; + if (this.state.nameError) { + nameError = <label className='control-label'>{this.state.nameError}</label>; + nameDivClass += ' has-error'; + } + + var serverError = null; + if (this.state.serverError) { + serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>; + } + + var disabled = false; + if (this.state.name.length <= 3) { + disabled = true; + } + + var button = null; + + if (this.props.service === Constants.GITLAB_SERVICE) { + button = ( + <a + className='btn btn-custom-login gitlab btn-full' + href='#' + onClick={this.handleSubmit} + disabled={disabled} + > + <span className='icon'/> + <span>Create {strings.Team} with GitLab Account</span> + </a> + ); + } + + return ( + <form + role='form' + onSubmit={this.handleSubmit} + > + <div className={nameDivClass}> + <input + autoFocus={true} + type='text' + ref='teamname' + className='form-control' + placeholder='Enter name of new team' + maxLength='128' + onChange={this.nameChange} + /> + {nameError} + </div> + <div className='form-group'> + {button} + {serverError} + </div> + <div className='form-group margin--extra-2x'> + <span><a href='/find_team'>{'Find my ' + strings.Team}</a></span> + </div> + </form> + ); + } +} + +SSOSignUpPage.defaultProps = { + service: '' +}; +SSOSignUpPage.propTypes = { + service: 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/pages/signup_team.jsx b/web/react/pages/signup_team.jsx index 37c441d4f..4b58025ac 100644 --- a/web/react/pages/signup_team.jsx +++ b/web/react/pages/signup_team.jsx @@ -5,11 +5,13 @@ var SignupTeam = require('../components/signup_team.jsx'); var AsyncClient = require('../utils/async_client.jsx'); -global.window.setup_signup_team_page = function() { +global.window.setup_signup_team_page = function(authServices) { AsyncClient.getConfig(); + var services = JSON.parse(authServices); + React.render( - <SignupTeam />, + <SignupTeam services={services} />, document.getElementById('signup-team') ); }; diff --git a/web/react/pages/signup_user_oauth.jsx b/web/react/pages/signup_user_oauth.jsx deleted file mode 100644 index 6a0707702..000000000 --- a/web/react/pages/signup_user_oauth.jsx +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. -// See License.txt for license information. - -var SignupUserOAuth = require('../components/signup_user_oauth.jsx'); - -global.window.setup_signup_user_oauth_page = function(user, team_name, team_display_name) { - React.render( - <SignupUserOAuth user={user} teamName={team_name} teamDisplayName={team_display_name} />, - document.getElementById('signup-user-complete') - ); -}; 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/client.jsx b/web/react/utils/client.jsx index 70220c71e..082f82a08 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -70,6 +70,21 @@ module.exports.createTeamFromSignup = function(teamSignup, success, error) { }); }; +module.exports.createTeamWithSSO = function(team, service, success, error) { + $.ajax({ + url: '/api/v1/teams/create_with_sso/' + service, + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(team), + success: success, + error: function onError(xhr, status, err) { + var e = handleError('createTeamWithSSO', xhr, status, err); + error(e); + } + }); +}; + module.exports.createUser = function(user, data, emailHash, success, error) { $.ajax({ url: '/api/v1/users/create?d=' + encodeURIComponent(data) + '&h=' + encodeURIComponent(emailHash), diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 82fc3da22..8721ced7c 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -60,7 +60,7 @@ module.exports = { DEFAULT_CHANNEL: 'town-square', OFFTOPIC_CHANNEL: 'off-topic', GITLAB_SERVICE: 'gitlab', - GOOGLE_SERVICE: 'google', + EMAIL_SERVICE: 'email', POST_CHUNK_SIZE: 60, MAX_POST_CHUNKS: 3, POST_LOADING: 'loading', diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index f0cf17446..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,10 +317,10 @@ 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); - $('.post-list-holder-by-time').perfectScrollbar('update'); } if (config.GoogleDeveloperKey) { @@ -304,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'> @@ -457,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 && @@ -1002,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('__'); |