diff options
Diffstat (limited to 'web')
58 files changed, 956 insertions, 833 deletions
diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx new file mode 100644 index 000000000..b23b3213f --- /dev/null +++ b/web/react/components/access_history_modal.jsx @@ -0,0 +1,100 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var UserStore = require('../stores/user_store.jsx'); +var AsyncClient = require('../utils/async_client.jsx'); +var Utils = require('../utils/utils.jsx'); + +function getStateFromStoresForAudits() { + return { + audits: UserStore.getAudits() + }; +} + +module.exports = React.createClass({ + componentDidMount: function() { + UserStore.addAuditsChangeListener(this._onChange); + AsyncClient.getAudits(); + + var self = this; + $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', function(e) { + self.setState({ moreInfo: [] }); + }); + }, + componentWillUnmount: function() { + UserStore.removeAuditsChangeListener(this._onChange); + }, + _onChange: function() { + this.setState(getStateFromStoresForAudits()); + }, + handleMoreInfo: function(index) { + var newMoreInfo = this.state.moreInfo; + newMoreInfo[index] = true; + this.setState({ moreInfo: newMoreInfo }); + }, + getInitialState: function() { + var initialState = getStateFromStoresForAudits(); + initialState.moreInfo = []; + return initialState; + }, + render: function() { + var accessList = []; + var currentHistoryDate = null; + + for (var i = 0; i < this.state.audits.length; i++) { + var currentAudit = this.state.audits[i]; + var newHistoryDate = new Date(currentAudit.create_at); + var newDate = null; + + if (!currentHistoryDate || currentHistoryDate.toLocaleDateString() !== newHistoryDate.toLocaleDateString()) { + currentHistoryDate = newHistoryDate; + newDate = (<div> {currentHistoryDate.toDateString()} </div>); + } + + accessList[i] = ( + <div className="access-history__table"> + <div className="access__date">{newDate}</div> + <div className="access__report"> + <div className="report__time">{newHistoryDate.toLocaleTimeString(navigator.language, {hour: '2-digit', minute:'2-digit'})}</div> + <div className="report__info"> + <div>{"IP: " + currentAudit.ip_address}</div> + { this.state.moreInfo[i] ? + <div> + <div>{"Session ID: " + currentAudit.session_id}</div> + <div>{"URL: " + currentAudit.action.replace("/api/v1", "")}</div> + </div> + : + <a href="#" onClick={this.handleMoreInfo.bind(this, i)}>More info</a> + } + </div> + {i < this.state.audits.length - 1 ? + <div className="divider-light"/> + : + null + } + </div> + </div> + ); + } + + return ( + <div> + <div className="modal fade" ref="modal" id="access-history" tabIndex="-1" role="dialog" aria-hidden="true"> + <div className="modal-dialog modal-lg"> + <div className="modal-content"> + <div className="modal-header"> + <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> + <h4 className="modal-title" id="myModalLabel">Access History</h4> + </div> + <div ref="modalBody" className="modal-body"> + <form role="form"> + { accessList } + </form> + </div> + </div> + </div> + </div> + </div> + ); + } +}); diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx new file mode 100644 index 000000000..d6f8f40eb --- /dev/null +++ b/web/react/components/activity_log_modal.jsx @@ -0,0 +1,116 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var UserStore = require('../stores/user_store.jsx'); +var Client = require('../utils/client.jsx'); +var AsyncClient = require('../utils/async_client.jsx'); + +function getStateFromStoresForSessions() { + return { + sessions: UserStore.getSessions(), + server_error: null, + client_error: null + }; +} + +module.exports = React.createClass({ + submitRevoke: function(altId) { + var self = this; + Client.revokeSession(altId, + function(data) { + AsyncClient.getSessions(); + }.bind(this), + function(err) { + state = getStateFromStoresForSessions(); + state.server_error = err; + this.setState(state); + }.bind(this) + ); + }, + componentDidMount: function() { + UserStore.addSessionsChangeListener(this._onChange); + AsyncClient.getSessions(); + + var self = this; + $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', function(e) { + self.setState({ moreInfo: [] }); + }); + }, + componentWillUnmount: function() { + UserStore.removeSessionsChangeListener(this._onChange); + }, + _onChange: function() { + this.setState(getStateFromStoresForSessions()); + }, + handleMoreInfo: function(index) { + var newMoreInfo = this.state.moreInfo; + newMoreInfo[index] = true; + this.setState({ moreInfo: newMoreInfo }); + }, + getInitialState: function() { + var initialState = getStateFromStoresForSessions(); + initialState.moreInfo = []; + return initialState; + }, + render: function() { + var activityList = []; + var server_error = this.state.server_error ? this.state.server_error : null; + + for (var i = 0; i < this.state.sessions.length; i++) { + var currentSession = this.state.sessions[i]; + var lastAccessTime = new Date(currentSession.last_activity_at); + var firstAccessTime = new Date(currentSession.create_at); + var devicePicture = ""; + + if (currentSession.props.platform === "Windows") { + devicePicture = "fa fa-windows"; + } + else if (currentSession.props.platform === "Macintosh" || currentSession.props.platform === "iPhone") { + devicePicture = "fa fa-apple"; + } + + activityList[i] = ( + <div className="activity-log__table"> + <div className="activity-log__report"> + <div className="report__platform"><i className={devicePicture} />{currentSession.props.platform}</div> + <div className="report__info"> + <div>{"Last activity: " + lastAccessTime.toDateString() + ", " + lastAccessTime.toLocaleTimeString()}</div> + { this.state.moreInfo[i] ? + <div> + <div>{"First time active: " + firstAccessTime.toDateString() + ", " + lastAccessTime.toLocaleTimeString()}</div> + <div>{"OS: " + currentSession.props.os}</div> + <div>{"Browser: " + currentSession.props.browser}</div> + <div>{"Session ID: " + currentSession.alt_id}</div> + </div> + : + <a href="#" onClick={this.handleMoreInfo.bind(this, i)}>More info</a> + } + </div> + </div> + <div className="activity-log__action"><button onClick={this.submitRevoke.bind(this, currentSession.alt_id)} className="btn btn-primary">Logout</button></div> + </div> + ); + } + + return ( + <div> + <div className="modal fade" ref="modal" id="activity-log" tabIndex="-1" role="dialog" aria-hidden="true"> + <div className="modal-dialog modal-lg"> + <div className="modal-content"> + <div className="modal-header"> + <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> + <h4 className="modal-title" id="myModalLabel">Active Devices</h4> + </div> + <div ref="modalBody" className="modal-body"> + <form role="form"> + { activityList } + </form> + { server_error } + </div> + </div> + </div> + </div> + </div> + ); + } +}); diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 0c23dcfac..a2448b569 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -31,6 +31,11 @@ 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.startsWith("^")) { + post.message = post.message.replace(/^\^+\s*/g, ""); + } + if (post.message.trim().length === 0 && this.state.previews.length === 0) { return; } @@ -68,6 +73,9 @@ 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, data.user_id, null); @@ -84,7 +92,13 @@ module.exports = React.createClass({ }.bind(this), function(err) { var state = {} - state.server_error = err.message; + + if (err.message === "Invalid RootId parameter") { + if ($('#post_deleted').length > 0) $('#post_deleted').modal('show'); + } else { + state.server_error = err.message; + } + state.submitting = false; this.setState(state); }.bind(this) @@ -92,6 +106,17 @@ 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(); @@ -112,6 +137,63 @@ module.exports = React.createClass({ handleUserInput: function(messageText) { 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 = {} @@ -174,10 +256,12 @@ 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(); @@ -194,6 +278,11 @@ module.exports = React.createClass({ this.setState({ channel_id: channel_id, messageText: messageText, initialText: messageText, submitting: false, 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(); @@ -204,7 +293,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 }; + return { channel_id: ChannelStore.getCurrentId(), messageText: messageText, uploadsInProgress: 0, previews: previews, submitting: false, initialText: messageText, caretCount: 0 }; }, setUploads: function(val) { var oldInProgress = this.state.uploadsInProgress @@ -234,7 +323,7 @@ module.exports = React.createClass({ }, render: function() { - 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 server_error = this.state.server_error ? <div className='has-error'><label className='control-label'>{ this.state.server_error }</label></div> : null; var post_error = this.state.post_error ? <label className='control-label'>{this.state.post_error}</label> : null; var limit_error = this.state.limit_error ? <div className='has-error'><label className='control-label'>{this.state.limit_error}</label></div> : null; diff --git a/web/react/components/delete_channel_modal.jsx b/web/react/components/delete_channel_modal.jsx index a8c690789..e23a37740 100644 --- a/web/react/components/delete_channel_modal.jsx +++ b/web/react/components/delete_channel_modal.jsx @@ -12,7 +12,7 @@ module.exports = React.createClass({ Client.deleteChannel(this.state.channel_id, function(data) { AsyncClient.getChannels(true); - window.location.href = '/channels/town-square'; + window.location.href = '/'; }.bind(this), function(err) { AsyncClient.dispatchError(err, "handleDelete"); diff --git a/web/react/components/file_preview.jsx b/web/react/components/file_preview.jsx index 17a1e2bc2..fdd12feec 100644 --- a/web/react/components/file_preview.jsx +++ b/web/react/components/file_preview.jsx @@ -16,20 +16,26 @@ module.exports = React.createClass({ var previews = []; this.props.files.forEach(function(filename) { + var originalFilename = filename; var filenameSplit = filename.split('.'); var ext = filenameSplit[filenameSplit.length-1]; var type = utils.getFileType(ext); + // This is a temporary patch to fix issue with old files using absolute paths + if (filename.indexOf("/api/v1/files/get") != -1) { + filename = filename.split("/api/v1/files/get")[1]; + } + filename = window.location.origin + "/api/v1/files/get" + filename; if (type === "image") { previews.push( - <div key={filename} className="preview-div" data-filename={filename}> + <div key={filename} className="preview-div" data-filename={originalFilename}> <img className="preview-img" src={filename}/> <a className="remove-preview" onClick={this.handleRemove}><i className="glyphicon glyphicon-remove"/></a> </div> ); } else { previews.push( - <div key={filename} className="preview-div custom-file" data-filename={filename}> + <div key={filename} className="preview-div custom-file" data-filename={originalFilename}> <div className={"file-icon "+utils.getIconClassName(type)}/> <a className="remove-preview" onClick={this.handleRemove}><i className="glyphicon glyphicon-remove"/></a> </div> diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index 74c7d4065..71fefff5b 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -1,102 +1,21 @@ // 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 TeamStore = require('../stores/team_store.jsx'); var BrowserStore = require('../stores/browser_store.jsx'); - - -var FindTeamDomain = React.createClass({ - handleSubmit: function(e) { - e.preventDefault(); - var state = { } - - var domain = this.refs.domain.getDOMNode().value.trim(); - if (!domain) { - state.server_error = "A domain is required" - this.setState(state); - return; - } - - if (!BrowserStore.isLocalStorageSupported()) { - state.server_error = "This service requires local storage to be enabled. Please enable it or exit private browsing."; - this.setState(state); - return; - } - - state.server_error = ""; - this.setState(state); - - client.findTeamByDomain(domain, - function(data) { - console.log(data); - if (data) { - window.location.href = window.location.protocol + "//" + domain + "." + utils.getDomainWithOutSub(); - } - else { - this.state.server_error = "We couldn't find your " + strings.Team + "."; - this.setState(this.state); - } - }.bind(this), - function(err) { - this.state.server_error = err.message; - this.setState(this.state); - }.bind(this) - ); - }, - getInitialState: function() { - return { }; - }, - render: function() { - var server_error = this.state.server_error ? <label className="control-label">{this.state.server_error}</label> : null; - - return ( - <div className="signup-team__container"> - <div> - <span className="signup-team__name">{ config.SiteName }</span> - <br/> - <span className="signup-team__subdomain">Enter your {strings.Team}'s domain.</span> - <br/> - <br/> - </div> - <form onSubmit={this.handleSubmit}> - <div className={server_error ? 'form-group has-error' : 'form-group'}> - { server_error } - <input type="text" className="form-control" name="domain" ref="domain" placeholder="team domain" /> - </div> - <div className="form-group"> - <button type="submit" className="btn btn-primary">Continue</button> - </div> - <div> - <span>Don't remember your {strings.Team}'s domain? <a href="/find_team">Find it here</a></span> - </div> - <br/> - <br/> - <br/> - <br/> - <br/> - <br/> - <div> - <span>{"Want to create your own " + strings.Team + "?"} <a href={utils.getHomeLink()} className="signup-team-login">Sign up now</a></span> - </div> - </form> - </div> - ); - } -}); +var Constants = require('../utils/constants.jsx'); module.exports = React.createClass({ handleSubmit: function(e) { e.preventDefault(); var state = { } - var domain = this.refs.domain.getDOMNode().value.trim(); - if (!domain) { - state.server_error = "A domain is required" + var name = this.props.teamName + if (!name) { + state.server_error = "Bad team name" this.setState(state); return; } @@ -124,23 +43,22 @@ module.exports = React.createClass({ state.server_error = ""; this.setState(state); - client.loginByEmail(domain, email, password, + client.loginByEmail(name, email, password, function(data) { - UserStore.setLastDomain(domain); - UserStore.setLastEmail(email); UserStore.setCurrentUser(data); + UserStore.setLastEmail(email); var redirect = utils.getUrlParameter("redirect"); if (redirect) { - window.location.href = decodeURI(redirect); + window.location.pathname = decodeURI(redirect); } else { - window.location.href = '/channels/town-square'; + window.location.pathname = '/' + name + '/channels/town-square'; } }.bind(this), function(err) { if (err.message == "Login failed because email address has not been verified") { - window.location.href = '/verify?domain=' + encodeURIComponent(domain) + '&email=' + encodeURIComponent(email); + window.location.href = '/verify_email?name=' + encodeURIComponent(name) + '&email=' + encodeURIComponent(email); return; } state.server_error = err.message; @@ -161,35 +79,35 @@ module.exports = React.createClass({ priorEmail = decodeURIComponent(emailParam); } - var subDomainClass = "form-control hidden"; - var subDomain = utils.getSubDomain(); + var teamDisplayName = this.props.teamDisplayName; + var teamName = this.props.teamName; - if (utils.isTestDomain()) { - subDomainClass = "form-control"; - subDomain = UserStore.getLastDomain(); - } else if (subDomain == "") { - return (<FindTeamDomain />); + var focusEmail = false; + var focusPassword = false; + if (priorEmail != "") { + focusPassword = true; + } else { + focusEmail = true; } return ( <div className="signup-team__container"> <div> - <span className="signup-team__name">{ subDomain }</span> + <span className="signup-team__name">{ teamDisplayName }</span> <br/> - <span className="signup-team__subdomain">{ utils.getDomainWithOutSub() }</span> + <span className="signup-team__subdomain">/{ teamName }/</span> <br/> <br/> </div> <form onSubmit={this.handleSubmit}> <div className={server_error ? 'form-group has-error' : 'form-group'}> { server_error } - <input type="text" className={subDomainClass} name="domain" defaultValue={subDomain} ref="domain" placeholder="Domain" /> </div> <div className={server_error ? 'form-group has-error' : 'form-group'}> - <input type="email" className="form-control" name="email" defaultValue={priorEmail} ref="email" placeholder="Email" /> + <input autoFocus={focusEmail} type="email" className="form-control" name="email" defaultValue={priorEmail} ref="email" placeholder="Email" /> </div> <div className={server_error ? 'form-group has-error' : 'form-group'}> - <input type="password" className="form-control" name="password" ref="password" placeholder="Password" /> + <input autoFocus={focusPassword} type="password" className="form-control" name="password" ref="password" placeholder="Password" /> </div> <div className="form-group"> <button type="submit" className="btn btn-primary">Sign in</button> @@ -198,10 +116,10 @@ module.exports = React.createClass({ <span><a href="/find_team">{"Find other " + strings.TeamPlural}</a></span> </div> <div className="form-group"> - <a href="/reset_password">I forgot my password</a> + <a href={"/" + teamName + "/reset_password"}>I forgot my password</a> </div> <div className="external-link"> - <span>{"Want to create your own " + strings.Team + "?"} <a href={utils.getHomeLink()} className="signup-team-login">Sign up now</a></span> + <span>{"Want to create your own " + strings.Team + "?"} <a href="/" className="signup-team-login">Sign up now</a></span> </div> </form> </div> diff --git a/web/react/components/member_list_team.jsx b/web/react/components/member_list_team.jsx index 6f1d83193..cb48e5cc5 100644 --- a/web/react/components/member_list_team.jsx +++ b/web/react/components/member_list_team.jsx @@ -5,6 +5,7 @@ var ChannelStore = require('../stores/channel_store.jsx'); var UserStore = require('../stores/user_store.jsx'); var Client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); +var utils = require('../utils/utils.jsx'); var MemberListTeamItem = React.createClass({ handleMakeMember: function() { diff --git a/web/react/components/msg_typing.jsx b/web/react/components/msg_typing.jsx index a6953028f..aacb315dd 100644 --- a/web/react/components/msg_typing.jsx +++ b/web/react/components/msg_typing.jsx @@ -51,7 +51,7 @@ module.exports = React.createClass({ }, render: function() { return ( - <span className="msg-typing">{ this.state.text }</span> + <span className="msg-typing">{ this.state.text }</span> ); } }); diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx index 78cf7d8b8..500fabb0e 100644 --- a/web/react/components/navbar.jsx +++ b/web/react/components/navbar.jsx @@ -7,6 +7,7 @@ var client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var UserStore = require('../stores/user_store.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); +var TeamStore = require('../stores/team_store.jsx'); var UserProfile = require('./user_profile.jsx'); var MessageWrapper = require('./message_wrapper.jsx'); @@ -27,7 +28,7 @@ function getCountsStateFromStores() { } else { if (channelMember.mention_count > 0) { count += channelMember.mention_count; - } else if (channel.total_msg_count - channelMember.msg_count > 0) { + } else if (channelMember.notify_level !== "quiet" && channel.total_msg_count - channelMember.msg_count > 0) { count += 1; } } @@ -61,93 +62,6 @@ var NotifyCounts = React.createClass({ } }); -var NavbarLoginForm = React.createClass({ - handleSubmit: function(e) { - e.preventDefault(); - var state = { }; - - var domain = this.refs.domain.getDOMNode().value.trim(); - if (!domain) { - state.server_error = "A domain is required"; - this.setState(state); - return; - } - - var email = this.refs.email.getDOMNode().value.trim(); - if (!email) { - state.server_error = "An email is required"; - this.setState(state); - return; - } - - var password = this.refs.password.getDOMNode().value.trim(); - if (!password) { - state.server_error = "A password is required"; - this.setState(state); - return; - } - - state.server_error = ""; - this.setState(state); - - client.loginByEmail(domain, email, password, - function(data) { - UserStore.setLastDomain(domain); - UserStore.setLastEmail(email); - UserStore.setCurrentUser(data); - - var redirect = utils.getUrlParameter("redirect"); - if (redirect) { - window.location.href = decodeURI(redirect); - } else { - window.location.href = '/channels/town-square'; - } - - }, - function(err) { - if (err.message == "Login failed because email address has not been verified") { - window.location.href = '/verify?domain=' + encodeURIComponent(domain) + '&email=' + encodeURIComponent(email); - return; - } - state.server_error = err.message; - this.valid = false; - this.setState(state); - }.bind(this) - ); - }, - getInitialState: function() { - return { }; - }, - render: function() { - var server_error = this.state.server_error ? <label className="control-label">{this.state.server_error}</label> : null; - - var subDomain = utils.getSubDomain(); - var subDomainClass = "form-control hidden"; - - if (subDomain == "") { - subDomain = UserStore.getLastDomain(); - subDomainClass = "form-control"; - } - - return ( - <form className="navbar-form navbar-right" onSubmit={this.handleSubmit}> - <a href="/find_team">Find your team</a> - <div className={server_error ? 'form-group has-error' : 'form-group'}> - { server_error } - <input type="text" className={subDomainClass} name="domain" defaultValue={subDomain} ref="domain" placeholder="Domain" /> - </div> - <div className={server_error ? 'form-group has-error' : 'form-group'}> - <input type="text" className="form-control" name="email" defaultValue={UserStore.getLastEmail()} ref="email" placeholder="Email" /> - </div> - <div className={server_error ? 'form-group has-error' : 'form-group'}> - <input type="password" className="form-control" name="password" ref="password" placeholder="Password" /> - </div> - <button type="submit" className="btn btn-default">Login</button> - </form> - ); - } -}); - function getStateFromStores() { return { channel: ChannelStore.getCurrent(), @@ -180,10 +94,10 @@ module.exports = React.createClass({ }, handleLeave: function(e) { client.leaveChannel(this.state.channel.id, - function() { + function(data, text, req) { AsyncClient.getChannels(true); - window.location.href = '/channels/town-square'; - }, + window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square'; + }.bind(this), function(err) { AsyncClient.dispatchError(err, "handleLeave"); } @@ -229,7 +143,7 @@ module.exports = React.createClass({ var currentId = UserStore.getCurrentId(); var popoverContent = ""; - var channelTitle = this.props.teamName; + var channelTitle = this.props.teamDisplayName; var isAdmin = false; var isDirect = false; var description = "" @@ -333,14 +247,8 @@ module.exports = React.createClass({ <div className="navbar-brand"> <a href="/" className="heading">{ channelTitle }</a> </div> - : null } + : "" } </div> - { !currentId ? - <div className="collapse navbar-collapse" id="navbar-collapse-1"> - <NavbarLoginForm /> - </div> - : null - } </div> </nav> ); diff --git a/web/react/components/new_channel.jsx b/web/react/components/new_channel.jsx index 069e0d6b1..49e088458 100644 --- a/web/react/components/new_channel.jsx +++ b/web/react/components/new_channel.jsx @@ -6,6 +6,8 @@ var utils = require('../utils/utils.jsx'); var client = require('../utils/client.jsx'); var asyncClient = require('../utils/async_client.jsx'); var UserStore = require('../stores/user_store.jsx'); +var TeamStore = require('../stores/team_store.jsx'); +var Constants = require('../utils/constants.jsx'); module.exports = React.createClass({ handleSubmit: function(e) { @@ -60,13 +62,13 @@ module.exports = React.createClass({ var self = this; client.createChannel(channel, - function(data) { + function() { this.refs.display_name.getDOMNode().value = ""; this.refs.channel_name.getDOMNode().value = ""; this.refs.channel_desc.getDOMNode().value = ""; $(self.refs.modal.getDOMNode()).modal('hide'); - window.location.href = "/channels/" + channel.name; + window.location = TeamStore.getCurrentTeamUrl() + "/channels/" + channel.name; asyncClient.getChannels(true); }.bind(this), function(err) { diff --git a/web/react/components/password_reset.jsx b/web/react/components/password_reset.jsx index 24566c7b1..b2edea620 100644 --- a/web/react/components/password_reset.jsx +++ b/web/react/components/password_reset.jsx @@ -10,13 +10,6 @@ SendResetPasswordLink = React.createClass({ e.preventDefault(); var state = {}; - var domain = this.refs.domain.getDOMNode().value.trim(); - if (!domain) { - state.error = "A domain is required" - this.setState(state); - return; - } - var email = this.refs.email.getDOMNode().value.trim(); if (!email) { state.error = "Please enter a valid email address." @@ -29,17 +22,17 @@ SendResetPasswordLink = React.createClass({ data = {}; data['email'] = email; - data['domain'] = domain; + data['name'] = this.props.teamName; client.sendPasswordReset(data, - function(data) { - this.setState({ error: null, update_text: <p>A password reset link has been sent to <b>{email}</b> for your <b>{this.props.teamName}</b> team on {config.SiteName}.com.</p>, more_update_text: "Please check your inbox." }); - $(this.refs.reset_form.getDOMNode()).hide(); - }.bind(this), - function(err) { - this.setState({ error: err.message, update_text: null, more_update_text: null }); - }.bind(this) - ); + function(data) { + this.setState({ error: null, update_text: <p>A password reset link has been sent to <b>{email}</b> for your <b>{this.props.teamDisplayName}</b> team on {window.location.hostname}.</p>, more_update_text: "Please check your inbox." }); + $(this.refs.reset_form.getDOMNode()).hide(); + }.bind(this), + function(err) { + this.setState({ error: err.message, update_text: null, more_update_text: null }); + }.bind(this) + ); }, getInitialState: function() { return {}; @@ -48,24 +41,13 @@ SendResetPasswordLink = React.createClass({ var update_text = this.state.update_text ? <div className="reset-form alert alert-success">{this.state.update_text}{this.state.more_update_text}</div> : null; var error = this.state.error ? <div className="form-group has-error"><label className="control-label">{this.state.error}</label></div> : null; - var subDomain = utils.getSubDomain(); - var subDomainClass = "form-control hidden"; - - if (subDomain == "") { - subDomain = UserStore.getLastDomain(); - subDomainClass = "form-control"; - } - return ( <div className="col-sm-12"> <div className="signup-team__container"> <h3>Password Reset</h3> { update_text } <form onSubmit={this.handleSendLink} ref="reset_form"> - <p>{"To reset your password, enter the email address you used to sign up for " + this.props.teamName + "."}</p> - <div className="form-group"> - <input type="text" className={subDomainClass} name="domain" defaultValue={subDomain} ref="domain" placeholder="Domain" /> - </div> + <p>{"To reset your password, enter the email address you used to sign up for " + this.props.teamDisplayName + "."}</p> <div className={error ? 'form-group has-error' : 'form-group'}> <input type="text" className="form-control" name="email" ref="email" placeholder="Email" /> </div> @@ -83,13 +65,6 @@ ResetPassword = React.createClass({ e.preventDefault(); var state = {}; - var domain = this.refs.domain.getDOMNode().value.trim(); - if (!domain) { - state.error = "A domain is required" - this.setState(state); - return; - } - var password = this.refs.password.getDOMNode().value.trim(); if (!password || password.length < 5) { state.error = "Please enter at least 5 characters." @@ -104,41 +79,30 @@ ResetPassword = React.createClass({ data['new_password'] = password; data['hash'] = this.props.hash; data['data'] = this.props.data; - data['domain'] = domain; + data['name'] = this.props.teamName; client.resetPassword(data, - function(data) { - this.setState({ error: null, update_text: "Your password has been updated successfully." }); - }.bind(this), - function(err) { - this.setState({ error: err.message, update_text: null }); - }.bind(this) - ); + function(data) { + this.setState({ error: null, update_text: "Your password has been updated successfully." }); + }.bind(this), + function(err) { + this.setState({ error: err.message, update_text: null }); + }.bind(this) + ); }, getInitialState: function() { return {}; }, render: function() { - var update_text = this.state.update_text ? <div className="form-group"><br/><label className="control-label reset-form">{this.state.update_text} Click <a href="/login">here</a> to log in.</label></div> : null; + var update_text = this.state.update_text ? <div className="form-group"><br/><label className="control-label reset-form">{this.state.update_text} Click <a href={"/" + this.props.teamName + "/login"}>here</a> to log in.</label></div> : null; var error = this.state.error ? <div className="form-group has-error"><label className="control-label">{this.state.error}</label></div> : null; - var subDomain = this.props.domain != "" ? this.props.domain : utils.getSubDomain(); - var subDomainClass = "form-control hidden"; - - if (subDomain == "") { - subDomain = UserStore.getLastDomain(); - subDomainClass = "form-control"; - } - return ( <div className="col-sm-12"> <div className="signup-team__container"> <h3>Password Reset</h3> <form onSubmit={this.handlePasswordReset}> - <p>{"Enter a new password for your " + this.props.teamName + " " + config.SiteName + " account."}</p> - <div className="form-group"> - <input type="text" className={subDomainClass} name="domain" defaultValue={subDomain} ref="domain" placeholder="Domain" /> - </div> + <p>{"Enter a new password for your " + this.props.teamDisplayName + " " + config.SiteName + " account."}</p> <div className={error ? 'form-group has-error' : 'form-group'}> <input type="password" className="form-control" name="password" ref="password" placeholder="Password" /> </div> @@ -161,14 +125,15 @@ module.exports = React.createClass({ if (this.props.isReset === "false") { return ( <SendResetPasswordLink + teamDisplayName={this.props.teamDisplayName} teamName={this.props.teamName} /> ); } else { return ( <ResetPassword + teamDisplayName={this.props.teamDisplayName} teamName={this.props.teamName} - domain={this.props.domain} hash={this.props.hash} data={this.props.data} /> diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx index e72a2d001..e3586ecde 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"> + <div className={"post__content" + (this.props.isActiveThread ? " active-thread__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_body.jsx b/web/react/components/post_body.jsx index d9678df30..7871f52b7 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -28,6 +28,12 @@ module.exports = React.createClass({ var type = utils.getFileType(fileInfo.ext); + // This is a temporary patch to fix issue with old files using absolute paths + if (fileInfo.path.indexOf("/api/v1/files/get") != -1) { + fileInfo.path = fileInfo.path.split("/api/v1/files/get")[1]; + } + fileInfo.path = window.location.origin + "/api/v1/files/get" + fileInfo.path; + if (type === "image") { $('<img/>').attr('src', fileInfo.path+'_thumb.jpg').load(function(path, name){ return function() { $(this).remove(); @@ -102,6 +108,12 @@ module.exports = React.createClass({ var type = utils.getFileType(fileInfo.ext); + // This is a temporary patch to fix issue with old files using absolute paths + if (fileInfo.path.indexOf("/api/v1/files/get") != -1) { + fileInfo.path = fileInfo.path.split("/api/v1/files/get")[1]; + } + fileInfo.path = window.location.origin + "/api/v1/files/get" + fileInfo.path; + if (type === "image") { if (i < Constants.MAX_DISPLAY_FILES) { postFiles.push( diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index c058455ba..8dc5013ca 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -22,7 +22,8 @@ function getStateFromStores() { return { post_list: PostStore.getCurrentPosts(), - channel: channel + channel: channel, + activeThreadRootId: "" }; } @@ -51,6 +52,7 @@ 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(); @@ -131,6 +133,7 @@ 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() { @@ -223,11 +226,15 @@ module.exports = React.createClass({ } }, _onTimeChange: function() { + if (!this.state.post_list) return; for (var id in this.state.post_list.posts) { if (!this.refs[id]) continue; this.refs[id].forceUpdateInfo(); } }, + _onActiveThreadChanged: function(rootId, parentId) { + this.setState({"activeThreadRootId": rootId}); + }, getMorePosts: function(e) { e.preventDefault(); @@ -347,8 +354,8 @@ module.exports = React.createClass({ if (ChannelStore.isDefault(channel)) { more_messages = ( <div className="channel-intro"> - <h4 className="channel-intro-title">Welcome</h4> - <p> + <h4 className="channel-intro__title">Beginning of {ui_name}</h4> + <p className="channel-intro__content"> Welcome to {ui_name}! <br/><br/> {"This is the first channel " + strings.Team + "mates see when they"} @@ -365,27 +372,27 @@ module.exports = React.createClass({ } else if (channel.name === Constants.OFFTOPIC_CHANNEL) { more_messages = ( <div className="channel-intro"> - <h4 className="channel-intro-title">Welcome</h4> - <p> + <h4 className="channel-intro__title">Beginning of {ui_name}</h4> + <p className="channel-intro__content"> {"This is the start of " + ui_name + ", a channel for conversations you’d prefer out of more focused channels."} <br/> - <a className="intro-links" href="#" style={userStyle} data-toggle="modal" data-target="#edit_channel" data-desc={channel.description} data-title={ui_name} data-channelid={channel.id}><i className="fa fa-pencil"></i>Set a description</a> </p> + <a className="intro-links" href="#" style={userStyle} data-toggle="modal" data-target="#edit_channel" data-desc={channel.description} data-title={ui_name} data-channelid={channel.id}><i className="fa fa-pencil"></i>Set a description</a> </div> ); } else { var ui_type = channel.type === 'P' ? "private group" : "channel"; more_messages = ( <div className="channel-intro"> - <h4 className="channel-intro-title">Welcome</h4> - <p> + <h4 className="channel-intro__title">Beginning of {ui_name}</h4> + <p className="channel-intro__content"> { creator_name != "" ? "This is the start of the " + ui_name + " " + ui_type + ", created by " + creator_name + " on " + utils.displayDate(channel.create_at) + "." : "This is the start of the " + ui_name + " " + ui_type + ", created on "+ utils.displayDate(channel.create_at) + "." } { channel.type === 'P' ? " Only invited members can see this private group." : " Any member can join and read this channel." } <br/> - <a className="intro-links" href="#" style={userStyle} data-toggle="modal" data-target="#edit_channel" data-desc={channel.description} data-title={channel.display_name} data-channelid={channel.id}><i className="fa fa-pencil"></i>Set a description</a> - <a className="intro-links" href="#" style={userStyle} data-toggle="modal" data-target="#channel_invite"><i className="fa fa-user-plus"></i>Invite others to this {ui_type}</a> </p> + <a className="intro-links" href="#" style={userStyle} data-toggle="modal" data-target="#edit_channel" data-desc={channel.description} data-title={channel.display_name} data-channelid={channel.id}><i className="fa fa-pencil"></i>Set a description</a> + <a className="intro-links" href="#" style={userStyle} data-toggle="modal" data-target="#channel_invite"><i className="fa fa-user-plus"></i>Invite others to this {ui_type}</a> </div> ); } @@ -419,7 +426,14 @@ 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); - var postCtl = <Post ref={post.id} sameUser={sameUser} sameRoot={sameRoot} post={post} parentPost={parentPost} key={post.id} posts={posts} hideProfilePic={hideProfilePic} isLastComment={isLastComment} />; + // 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} + /> + ); currentPostDay = utils.getDateForUnixTicks(post.create_at); if (currentPostDay.toDateString() != previousPostDay.toDateString()) { diff --git a/web/react/components/post_right.jsx b/web/react/components/post_right.jsx index 024bff26c..93f5d91b0 100644 --- a/web/react/components/post_right.jsx +++ b/web/react/components/post_right.jsx @@ -68,6 +68,7 @@ RootPost = React.createClass({ var filenames = this.props.post.filenames; var isOwner = UserStore.getCurrentId() == this.props.post.user_id; var timestamp = UserStore.getProfile(this.props.post.user_id).update_at; + var channel = ChannelStore.get(this.props.post.channel_id); var type = "Post"; if (this.props.post.root_id.length > 0) { @@ -79,6 +80,10 @@ RootPost = React.createClass({ currentUserCss = "current--user"; } + if (channel) { + channelName = (channel.type === 'D') ? "Private Message" : channel.display_name; + } + if (filenames) { var postFiles = []; var images = []; @@ -86,28 +91,27 @@ RootPost = React.createClass({ var re2 = new RegExp('\\(', 'g'); var re3 = new RegExp('\\)', 'g'); for (var i = 0; i < filenames.length && i < Constants.MAX_DISPLAY_FILES; i++) { - var fileSplit = filenames[i].split('.'); - if (fileSplit.length < 2) continue; - - var ext = fileSplit[fileSplit.length-1]; - fileSplit.splice(fileSplit.length-1,1); - var filePath = fileSplit.join('.'); - var filename = filePath.split('/')[filePath.split('/').length-1]; + var fileInfo = utils.splitFileLocation(filenames[i]); + var ftype = utils.getFileType(fileInfo.ext); - var ftype = utils.getFileType(ext); + // This is a temporary patch to fix issue with old files using absolute paths + if (fileInfo.path.indexOf("/api/v1/files/get") != -1) { + fileInfo.path = fileInfo.path.split("/api/v1/files/get")[1]; + } + fileInfo.path = window.location.origin + "/api/v1/files/get" + fileInfo.path; if (ftype === "image") { - var url = filePath.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29'); + var url = fileInfo.path.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29'); postFiles.push( - <div className="post-image__column" key={filePath}> - <a href="#" onClick={this.handleImageClick} data-img-id={images.length.toString()} data-toggle="modal" data-target={"#" + postImageModalId }><div ref={filePath} className="post__image" style={{backgroundImage: 'url(' + url + '_thumb.jpg)'}}></div></a> + <div className="post-image__column" key={fileInfo.path}> + <a href="#" onClick={this.handleImageClick} data-img-id={images.length.toString()} data-toggle="modal" data-target={"#" + postImageModalId }><div ref={fileInfo.path} className="post__image" style={{backgroundImage: 'url(' + url + '_thumb.jpg)'}}></div></a> </div> ); images.push(filenames[i]); } else { postFiles.push( - <div className="post-image__column custom-file" key={filePath}> - <a href={filePath+"."+ext} download={filename+"."+ext}> + <div className="post-image__column custom-file" key={fileInfo.path}> + <a href={fileInfo.path+"."+ext} download={fileInfo.name+"."+ext}> <div className={"file-icon "+utils.getIconClassName(ftype)}/> </a> </div> @@ -118,6 +122,7 @@ RootPost = React.createClass({ return ( <div className={"post post--root " + currentUserCss}> + <div className="post-right-channel__name">{ channelName }</div> <div className="post-profile-img__container"> <img className="post-profile-img" src={"/api/v1/users/" + this.props.post.user_id + "/image?time=" + timestamp} height="36" width="36" /> </div> @@ -195,28 +200,28 @@ CommentPost = React.createClass({ var re2 = new RegExp('\\(', 'g'); var re3 = new RegExp('\\)', 'g'); for (var i = 0; i < filenames.length && i < Constants.MAX_DISPLAY_FILES; i++) { - var fileSplit = filenames[i].split('.'); - if (fileSplit.length < 2) continue; - var ext = fileSplit[fileSplit.length-1]; - fileSplit.splice(fileSplit.length-1,1) - var filePath = fileSplit.join('.'); - var filename = filePath.split('/')[filePath.split('/').length-1]; + var fileInfo = utils.splitFileLocation(filenames[i]); + var type = utils.getFileType(fileInfo.ext); - var type = utils.getFileType(ext); + // This is a temporary patch to fix issue with old files using absolute paths + if (fileInfo.path.indexOf("/api/v1/files/get") != -1) { + fileInfo.path = fileInfo.path.split("/api/v1/files/get")[1]; + } + fileInfo.path = window.location.origin + "/api/v1/files/get" + fileInfo.path; if (type === "image") { - var url = filePath.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29'); + var url = fileInfo.path.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29'); postFiles.push( - <div className="post-image__column" key={filename}> - <a href="#" onClick={this.handleImageClick} data-img-id={images.length.toString()} data-toggle="modal" data-target={"#" + postImageModalId }><div ref={filename} className="post__image" style={{backgroundImage: 'url(' + url + '_thumb.jpg)'}}></div></a> + <div className="post-image__column" key={fileInfo.path}> + <a href="#" onClick={this.handleImageClick} data-img-id={images.length.toString()} data-toggle="modal" data-target={"#" + postImageModalId }><div ref={fileInfo.path} className="post__image" style={{backgroundImage: 'url(' + url + '_thumb.jpg)'}}></div></a> </div> ); images.push(filenames[i]); } else { postFiles.push( - <div className="post-image__column custom-file" key={filename}> - <a href={filePath+"."+ext} download={filename+"."+ext}> + <div className="post-image__column custom-file" key={fileInfo.path}> + <a href={fileInfo.path+"."+fileInfo.ext} download={fileInfo.name+"."+fileInfo.ext}> <div className={"file-icon "+utils.getIconClassName(type)}/> </a> </div> @@ -288,6 +293,8 @@ module.exports = React.createClass({ }); }, componentDidUpdate: function() { + $(".post-right__scroll").scrollTop($(".post-right__scroll")[0].scrollHeight); + $(".post-right__scroll").perfectScrollbar('update'); this.resize(); }, componentWillUnmount: function() { @@ -346,6 +353,7 @@ module.exports = React.createClass({ $(".post-right__scroll").css("height", height + "px"); $(".post-right__scroll").scrollTop(100000); $(".post-right__scroll").perfectScrollbar(); + $(".post-right__scroll").perfectScrollbar('update'); }, render: function() { diff --git a/web/react/components/rename_channel_modal.jsx b/web/react/components/rename_channel_modal.jsx index b4ccb2937..2ae331626 100644 --- a/web/react/components/rename_channel_modal.jsx +++ b/web/react/components/rename_channel_modal.jsx @@ -6,6 +6,8 @@ var utils = require('../utils/utils.jsx'); var Client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); +var TeamStore = require('../stores/team_store.jsx'); +var Constants = require('../utils/constants.jsx'); module.exports = React.createClass({ handleSubmit: function(e) { @@ -60,12 +62,12 @@ module.exports = React.createClass({ return; Client.updateChannel(channel, - function(data) { + function(data, text, req) { this.refs.display_name.getDOMNode().value = ""; this.refs.channel_name.getDOMNode().value = ""; $('#' + this.props.modalId).modal('hide'); - window.location.href = '/channels/' + this.state.channel_name; + window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + this.state.channel_name; AsyncClient.getChannels(true); }.bind(this), function(err) { diff --git a/web/react/components/rename_team_modal.jsx b/web/react/components/rename_team_modal.jsx index 67a150b9d..a6da57b67 100644 --- a/web/react/components/rename_team_modal.jsx +++ b/web/react/components/rename_team_modal.jsx @@ -24,13 +24,13 @@ module.exports = React.createClass({ if (!valid) return; - if (this.props.teamName === name) + if (this.props.teamDisplayName === name) return; var data = {}; data["new_name"] = name; - Client.updateTeamName(data, + Client.updateTeamDisplayName(data, function(data) { $('#rename_team_link').modal('hide'); window.location.reload(); @@ -47,11 +47,11 @@ module.exports = React.createClass({ componentDidMount: function() { var self = this; $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', function(e) { - self.setState({ name: self.props.teamName }); + self.setState({ name: self.props.teamDisplayName }); }); }, getInitialState: function() { - return { name: this.props.teamName }; + return { name: this.props.teamDisplayName }; }, render: function() { diff --git a/web/react/components/search_results.jsx b/web/react/components/search_results.jsx index 1fd974642..643ad112b 100644 --- a/web/react/components/search_results.jsx +++ b/web/react/components/search_results.jsx @@ -77,7 +77,7 @@ var SearchItem = React.createClass({ var message = utils.textToJsx(this.props.post.message, {searchTerm: this.props.term, noMentionHighlight: !this.props.isMentionSearch}); var channelName = ""; - var channel = ChannelStore.get(this.props.post.channel_id) + var channel = ChannelStore.get(this.props.post.channel_id); var timestamp = UserStore.getCurrentUser().update_at; if (channel) { diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 65727c597..3cf67e410 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -6,6 +6,7 @@ var ChannelStore = require('../stores/channel_store.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var SocketStore = require('../stores/socket_store.jsx'); var UserStore = require('../stores/user_store.jsx'); +var TeamStore = require('../stores/team_store.jsx'); var utils = require('../utils/utils.jsx'); var SidebarHeader = require('./sidebar_header.jsx'); var SearchBox = require('./search_bar.jsx'); @@ -13,93 +14,6 @@ var SearchBox = require('./search_bar.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; -var SidebarLoginForm = React.createClass({ - handleSubmit: function(e) { - e.preventDefault(); - var state = { } - - var domain = this.refs.domain.getDOMNode().value.trim(); - if (!domain) { - state.server_error = "A domain is required" - this.setState(state); - return; - } - - var email = this.refs.email.getDOMNode().value.trim(); - if (!email) { - state.server_error = "An email is required" - this.setState(state); - return; - } - - var password = this.refs.password.getDOMNode().value.trim(); - if (!password) { - state.server_error = "A password is required" - this.setState(state); - return; - } - - state.server_error = ""; - this.setState(state); - - client.loginByEmail(domain, email, password, - function(data) { - UserStore.setLastDomain(domain); - UserStore.setLastEmail(email); - UserStore.setCurrentUser(data); - - var redirect = utils.getUrlParameter("redirect"); - if (redirect) { - window.location.href = decodeURI(redirect); - } else { - window.location.href = '/channels/town-square'; - } - - }.bind(this), - function(err) { - if (err.message == "Login failed because email address has not been verified") { - window.location.href = '/verify?domain=' + encodeURIComponent(domain) + '&email=' + encodeURIComponent(email); - return; - } - state.server_error = err.message; - this.valid = false; - this.setState(state); - }.bind(this) - ); - }, - getInitialState: function() { - return { }; - }, - render: function() { - var server_error = this.state.server_error ? <label className="control-label">{this.state.server_error}</label> : null; - - var subDomain = utils.getSubDomain(); - var subDomainClass = "form-control hidden"; - - if (subDomain == "") { - subDomain = UserStore.getLastDomain(); - subDomainClass = "form-control"; - } - - return ( - <form className="" onSubmit={this.handleSubmit}> - <a href="/find_team">{"Find your " + strings.Team}</a> - <div className={server_error ? 'form-group has-error' : 'form-group'}> - { server_error } - <input type="text" className={subDomainClass} name="domain" defaultValue={subDomain} ref="domain" placeholder="Domain" /> - </div> - <div className={server_error ? 'form-group has-error' : 'form-group'}> - <input type="text" className="form-control" name="email" defaultValue={UserStore.getLastEmail()} ref="email" placeholder="Email" /> - </div> - <div className={server_error ? 'form-group has-error' : 'form-group'}> - <input type="password" className="form-control" name="password" ref="password" placeholder="Password" /> - </div> - <button type="submit" className="btn btn-default">Login</button> - </form> - ); - } -}); - function getStateFromStores() { var members = ChannelStore.getAllMembers(); var team_member_map = UserStore.getActiveOnlyProfiles(); @@ -192,7 +106,7 @@ function getStateFromStores() { }; } -var SidebarLoggedIn = React.createClass({ +module.exports = React.createClass({ componentDidMount: function() { ChannelStore.addChangeListener(this._onChange); UserStore.addChangeListener(this._onChange); @@ -383,7 +297,7 @@ var SidebarLoggedIn = React.createClass({ ); } else { return ( - <li key={channel.name} className={active}><a className={"sidebar-channel " + titleClass} href={"/channels/"+channel.name}><span className="status" dangerouslySetInnerHTML={{__html: statusIcon}} /> {badge}{channel.display_name}</a></li> + <li key={channel.name} className={active}><a className={"sidebar-channel " + titleClass} href={TeamStore.getCurrentTeamUrl() + "/channels/"+channel.name}><span className="status" dangerouslySetInnerHTML={{__html: statusIcon}} /> {badge}{channel.display_name}</a></li> ); } @@ -414,7 +328,7 @@ var SidebarLoggedIn = React.createClass({ } return ( <div> - <SidebarHeader teamName={this.props.teamName} teamType={this.props.teamType} /> + <SidebarHeader teamDisplayName={this.props.teamDisplayName} teamType={this.props.teamType} /> <SearchBox /> <div className="nav-pills__container"> @@ -440,25 +354,3 @@ var SidebarLoggedIn = React.createClass({ ); } }); - -var SidebarLoggedOut = React.createClass({ - render: function() { - return ( - <div> - <SidebarHeader teamName={this.props.teamName} /> - <SidebarLoginForm /> - </div> - ); - } -}); - -module.exports = React.createClass({ - render: function() { - var currentId = UserStore.getCurrentId(); - if (currentId != null) { - return <SidebarLoggedIn teamName={this.props.teamName} teamType={this.props.teamType} />; - } else { - return <SidebarLoggedOut teamName={this.props.teamName} />; - } - } -}); diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx index 45c9ca629..7a7e92854 100644 --- a/web/react/components/sidebar_header.jsx +++ b/web/react/components/sidebar_header.jsx @@ -61,19 +61,15 @@ var NavbarDropdown = React.createClass({ var teams = []; + teams.push(<li className="divider" key="div"></li>); if (this.state.teams.length > 1) { for (var i = 0; i < this.state.teams.length; i++) { - var domain = this.state.teams[i]; + var teamName = this.state.teams[i]; - if (domain == utils.getSubDomain()) - continue; - - if (teams.length == 0) - teams.push(<li className="divider" key="div"></li>); - - teams.push(<li key={ domain }><a href={window.location.protocol + "//" + domain + "." + utils.getDomainWithOutSub() }>Switch to { domain }</a></li>); + teams.push(<li key={ teamName }><a href={window.location.origin + "/" + teamName }>Switch to { teamName }</a></li>); } } + teams.push(<li><a href={window.location.origin + "/signup_team" }>Create a New Team</a></li>); return ( <ul className="nav navbar-nav navbar-right"> @@ -110,19 +106,25 @@ module.exports = React.createClass({ }, render: function() { - var me = UserStore.getCurrentUser(); - + var teamDisplayName = this.props.teamDisplayName ? this.props.teamDisplayName : config.SiteName; + var me = UserStore.getCurrentUser() if (!me) { return null; } return ( <div className="team__header theme"> - <img className="user__picture" src={"/api/v1/users/" + me.id + "/image?time=" + me.update_at} /> - <div className="header__info"> - <div className="user__name">{'@' + me.username}</div> - <a className="team__name" href="/channels/town-square">{this.props.teamName}</a> - </div> + <a className="settings_link" href="#" data-toggle="modal" data-target="#user_settings1"> + { me.last_picture_update ? + <img className="user__picture" src={"/api/v1/users/" + me.id + "/image?time=" + me.update_at} /> + : + <div /> + } + <div className="header__info"> + <div className="user__name">{ '@' + me.username}</div> + <div className="team__name">{ teamDisplayName }</div> + </div> + </a> <NavbarDropdown teamType={this.props.teamType} /> </div> ); diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx index 22d1d9ad2..15306a499 100644 --- a/web/react/components/sidebar_right_menu.jsx +++ b/web/react/components/sidebar_right_menu.jsx @@ -49,12 +49,12 @@ module.exports = React.createClass({ } var siteName = config.SiteName != null ? config.SiteName : ""; - var teamName = this.props.teamName ? this.props.teamName : siteName; + var teamDisplayName = this.props.teamDisplayName ? this.props.teamDisplayName : siteName; return ( <div> <div className="team__header theme"> - <a className="team__name" href="/channels/town-square">{ teamName }</a> + <a className="team__name" href="/channels/town-square">{ teamDisplayName }</a> </div> <div className="nav-pills__container"> diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx index 22086250c..cf982cc1e 100644 --- a/web/react/components/signup_team.jsx +++ b/web/react/components/signup_team.jsx @@ -20,8 +20,8 @@ module.exports = React.createClass({ state.email_error = ""; } - team.name = this.refs.name.getDOMNode().value.trim(); - if (!team.name) { + team.display_name = this.refs.name.getDOMNode().value.trim(); + if (!team.display_name) { state.name_error = "This field is required"; state.inValid = true; } @@ -34,7 +34,7 @@ module.exports = React.createClass({ return; } - client.signupTeam(team.email, team.name, + client.signupTeam(team.email, team.display_name, function(data) { if (data["follow_link"]) { window.location.href = data["follow_link"]; @@ -61,7 +61,7 @@ module.exports = React.createClass({ return ( <form role="form" onSubmit={this.handleSubmit}> <div className={ email_error ? "form-group has-error" : "form-group" }> - <input type="email" ref="email" className="form-control" placeholder="Email Address" maxLength="128" /> + <input autoFocus={true} type="email" ref="email" className="form-control" placeholder="Email Address" maxLength="128" /> { email_error } </div> <div className={ name_error ? "form-group has-error" : "form-group" }> @@ -70,6 +70,9 @@ module.exports = React.createClass({ </div> { server_error } <button className="btn btn-md btn-primary" type="submit">Sign up for Free</button> + <div className="form-group form-group--small"> + <span><a href="/find_team">{"Find my " + strings.Team}</a></span> + </div> </form> ); } diff --git a/web/react/components/signup_team_complete.jsx b/web/react/components/signup_team_complete.jsx index 9e2a13955..9ceeb6324 100644 --- a/web/react/components/signup_team_complete.jsx +++ b/web/react/components/signup_team_complete.jsx @@ -15,7 +15,7 @@ WelcomePage = React.createClass({ return; } e.preventDefault(); - this.props.state.wizard = "team_name"; + this.props.state.wizard = "team_display_name"; this.props.updateParent(this.props.state); }, handleDiffEmail: function (e) { @@ -57,6 +57,17 @@ WelcomePage = React.createClass({ getInitialState: function() { return { use_diff: false }; }, + handleKeyPress: function(event) { + if (event.keyCode == 13) { + this.submitNext(event); + } + }, + componentWillMount: function() { + document.addEventListener("keyup", this.handleKeyPress, false); + }, + componentWillUnmount: function() { + document.removeEventListener("keyup", this.handleKeyPress, false); + }, render: function() { client.track('signup', 'signup_team_01_welcome'); @@ -77,7 +88,7 @@ WelcomePage = React.createClass({ <span className="black">{ this.props.state.team.email }</span><br /> </p> <div className="form-group"> - <button className="btn-primary btn form-group" onClick={this.submitNext}><i className="glyphicon glyphicon-ok"></i>Yes, this address is correct</button> + <button className="btn-primary btn form-group" type="submit" onClick={this.submitNext}><i className="glyphicon glyphicon-ok"></i>Yes, this address is correct</button> { storage_error } </div> <hr /> @@ -92,15 +103,15 @@ WelcomePage = React.createClass({ { email_error } </div> { server_error } - <button className="btn btn-md btn-primary" onClick={this.handleDiffSubmit} type="submit">Use this instead</button> + <button className="btn btn-md btn-primary" type="button" onClick={this.handleDiffSubmit} type="submit">Use this instead</button> </div> - <button onClick={this.handleDiffEmail} className={ this.state.use_diff ? "btn-default btn hidden" : "btn-default btn" }>Use a different address</button> + <button type="button" onClick={this.handleDiffEmail} className={ this.state.use_diff ? "btn-default btn hidden" : "btn-default btn" }>Use a different address</button> </div> ); } }); -TeamNamePage = React.createClass({ +TeamDisplayNamePage = React.createClass({ submitBack: function (e) { e.preventDefault(); this.props.state.wizard = "welcome"; @@ -109,19 +120,24 @@ TeamNamePage = React.createClass({ submitNext: function (e) { e.preventDefault(); - var name = this.refs.name.getDOMNode().value.trim(); - if (!name) { + var display_name = this.refs.name.getDOMNode().value.trim(); + if (!display_name) { this.setState({name_error: "This field is required"}); return; } this.props.state.wizard = "team_url"; - this.props.state.team.name = name; + this.props.state.team.display_name = display_name; this.props.updateParent(this.props.state); }, getInitialState: function() { return { }; }, + handleFocus: function(e) { + e.preventDefault(); + + e.currentTarget.select(); + }, render: function() { client.track('signup', 'signup_team_02_name'); @@ -130,29 +146,31 @@ TeamNamePage = React.createClass({ return ( <div> + <form> <img className="signup-team-logo" src="/static/images/logo.png" /> <h2>{utils.toTitleCase(strings.Team) + " Name"}</h2> <div className={ name_error ? "form-group has-error" : "form-group" }> <div className="row"> <div className="col-sm-9"> - <input type="text" ref="name" className="form-control" placeholder="" maxLength="128" defaultValue={this.props.state.team.name} /> + <input type="text" ref="name" className="form-control" placeholder="" maxLength="128" defaultValue={this.props.state.team.display_name} autoFocus={true} onFocus={this.handleFocus} /> </div> </div> { name_error } </div> <p>{"Your " + strings.Team + " name shows in menus and headings. It may include the name of your " + strings.Company + ", but it's not required."}</p> - <button className="btn btn-default" onClick={this.submitBack}><i className="glyphicon glyphicon-chevron-left"></i> Back</button> - <button className="btn-primary btn" onClick={this.submitNext}>Next<i className="glyphicon glyphicon-chevron-right"></i></button> + <button type="button" className="btn btn-default" onClick={this.submitBack}><i className="glyphicon glyphicon-chevron-left"></i> Back</button> + <button type="submit" className="btn-primary btn" onClick={this.submitNext}>Next<i className="glyphicon glyphicon-chevron-right"></i></button> + </form> </div> ); } }); -TeamUrlPage = React.createClass({ +TeamURLPage = React.createClass({ submitBack: function (e) { e.preventDefault(); - this.props.state.wizard = "team_name"; + this.props.state.wizard = "team_display_name"; this.props.updateParent(this.props.state); }, submitNext: function (e) { @@ -172,18 +190,18 @@ TeamUrlPage = React.createClass({ return; } else if (cleaned_name.length <= 3 || cleaned_name.length > 15) { - this.setState({name_error: "Domain must be 4 or more characters up to a maximum of 15"}) + this.setState({name_error: "Name must be 4 or more characters up to a maximum of 15"}) return; } - for (var index = 0; index < constants.RESERVED_DOMAINS.length; index++) { - if (cleaned_name.indexOf(constants.RESERVED_DOMAINS[index]) == 0) { - this.setState({name_error: "This Team URL name is unavailable"}) + for (var index = 0; index < constants.RESERVED_TEAM_NAMES.length; index++) { + if (cleaned_name.indexOf(constants.RESERVED_TEAM_NAMES[index]) == 0) { + this.setState({name_error: "This team name is unavailable"}) return; } } - client.findTeamByDomain(name, + client.findTeamByName(name, function(data) { if (!data) { if (config.AllowSignupDomainsWizard) { @@ -193,7 +211,7 @@ TeamUrlPage = React.createClass({ this.props.state.team.type = 'O'; } - this.props.state.team.domain = name; + this.props.state.team.name = name; this.props.updateParent(this.props.state); } else { @@ -210,6 +228,11 @@ TeamUrlPage = React.createClass({ getInitialState: function() { return { }; }, + handleFocus: function(e) { + e.preventDefault(); + + e.currentTarget.select(); + }, render: function() { client.track('signup', 'signup_team_03_url'); @@ -218,14 +241,15 @@ TeamUrlPage = React.createClass({ return ( <div> + <form> <img className="signup-team-logo" src="/static/images/logo.png" /> <h2>{utils.toTitleCase(strings.Team) + " URL"}</h2> <div className={ name_error ? "form-group has-error" : "form-group" }> <div className="row"> <div className="col-sm-9"> <div className="input-group"> - <input type="text" ref="name" className="form-control text-right" placeholder="" maxLength="128" defaultValue={this.props.state.team.domain} /> - <span className="input-group-addon">.{ utils.getDomainWithOutSub() }</span> + <span className="input-group-addon">{ window.location.origin + "/" }</span> + <input type="text" ref="name" className="form-control" placeholder="" maxLength="128" defaultValue={this.props.state.team.name} autoFocus={true} onFocus={this.handleFocus}/> </div> </div> </div> @@ -233,8 +257,9 @@ TeamUrlPage = React.createClass({ </div> <p className="black">{"Pick something short and memorable for your " + strings.Team + "'s web address."}</p> <p>{"Your " + strings.Team + " URL can only contain lowercase letters, numbers and dashes. Also, it needs to start with a letter and cannot end in a dash."}</p> - <button className="btn btn-default" onClick={this.submitBack}><i className="glyphicon glyphicon-chevron-left"></i> Back</button> - <button className="btn-primary btn" onClick={this.submitNext}>Next<i className="glyphicon glyphicon-chevron-right"></i></button> + <button type="button" className="btn btn-default" onClick={this.submitBack}><i className="glyphicon glyphicon-chevron-left"></i> Back</button> + <button type="submit" className="btn-primary btn" onClick={this.submitNext}>Next<i className="glyphicon glyphicon-chevron-right"></i></button> + </form> </div> ); } @@ -291,6 +316,7 @@ AllowedDomainsPage = React.createClass({ return ( <div> + <form> <img className="signup-team-logo" src="/static/images/logo.png" /> <h2>Email Domain</h2> <p> @@ -303,7 +329,7 @@ AllowedDomainsPage = React.createClass({ <div className="col-sm-9"> <div className="input-group"> <span className="input-group-addon">@</span> - <input type="text" ref="name" className="form-control" placeholder="" maxLength="128" defaultValue={this.props.state.team.allowed_domains} /> + <input type="text" ref="name" className="form-control" placeholder="" maxLength="128" defaultValue={this.props.state.team.allowed_domains} autoFocus={true} onFocus={this.handleFocus}/> </div> </div> </div> @@ -313,8 +339,9 @@ AllowedDomainsPage = React.createClass({ <p> <div className="checkbox"><label><input type="checkbox" ref="open_network" defaultChecked={this.props.state.team.type == 'O'} /> Allow anyone to signup to this domain without an invitation.</label></div> </p> - <button className="btn btn-default" onClick={this.submitBack}><i className="glyphicon glyphicon-chevron-left"></i> Back</button> - <button className="btn-primary btn" onClick={this.submitNext}>Next<i className="glyphicon glyphicon-chevron-right"></i></button> + <button type="button" className="btn btn-default" onClick={this.submitBack}><i className="glyphicon glyphicon-chevron-left"></i> Back</button> + <button type="submit" className="btn-primary btn" onClick={this.submitNext}>Next<i className="glyphicon glyphicon-chevron-right"></i></button> + </form> </div> ); } @@ -356,7 +383,7 @@ EmailItem = React.createClass({ return ( <div className={ email_error ? "form-group has-error" : "form-group" }> - <input type="email" ref="email" className="form-control" placeholder="Email Address" defaultValue={this.props.email} maxLength="128" /> + <input autoFocus={this.props.focus} type="email" ref="email" className="form-control" placeholder="Email Address" defaultValue={this.props.email} maxLength="128" /> { email_error } </div> ); @@ -424,16 +451,22 @@ SendInivtesPage = React.createClass({ var emails = []; for (var i = 0; i < this.props.state.invites.length; i++) { - emails.push(<EmailItem key={i} ref={'email_' + i} email={this.props.state.invites[i]} />); + if (i == 0) { + emails.push(<EmailItem focus={true} key={i} ref={'email_' + i} email={this.props.state.invites[i]} />); + } else { + emails.push(<EmailItem focus={false} key={i} ref={'email_' + i} email={this.props.state.invites[i]} />); + } } return ( <div> + <form> <img className="signup-team-logo" src="/static/images/logo.png" /> <h2>Send Invitations</h2> { emails } - <div className="form-group"><button className="btn-default btn" onClick={this.submitAddInvite}>Add Invitation</button></div> - <div className="form btn-default-group"><button className="btn btn-default" onClick={this.submitBack}><i className="glyphicon glyphicon-chevron-left"></i> Back</button> <button className="btn-primary btn" onClick={this.submitNext}>Next<i className="glyphicon glyphicon-chevron-right"></i></button></div> + <div className="form-group"><button type="button" className="btn-default btn" onClick={this.submitAddInvite}>Add Invitation</button></div> + <div className="form btn-default-group"><button type="button" className="btn btn-default" onClick={this.submitBack}><i className="glyphicon glyphicon-chevron-left"></i> Back</button> <button type="submit" className="btn-primary btn" onClick={this.submitNext}>Next<i className="glyphicon glyphicon-chevron-right"></i></button></div> + </form> <p>{"If you'd prefer, you can send invitations after you finish setting up the "+ strings.Team + "."}</p> <div><a href="#" onClick={this.submitSkip}>Skip this step</a></div> </div> @@ -477,20 +510,22 @@ UsernamePage = React.createClass({ return ( <div> + <form> <img className="signup-team-logo" src="/static/images/logo.png" /> <h2>Choose a username</h2> <div className={ name_error ? "form-group has-error" : "form-group" }> <div className="row"> <div className="col-sm-9"> - <input type="text" ref="name" className="form-control" placeholder="" defaultValue={this.props.state.user.username} maxLength="128" /> + <input autoFocus={true} type="text" ref="name" className="form-control" placeholder="" defaultValue={this.props.state.user.username} maxLength="128" /> </div> </div> { name_error } </div> <p>{"Pick something " + strings.Team + "mates will recognize. Your username is how you will appear to others."}</p> <p>It can be made of lowercase letters and numbers.</p> - <button className="btn btn-default" onClick={this.submitBack}><i className="glyphicon glyphicon-chevron-left"></i> Back</button> - <button className="btn-primary btn" onClick={this.submitNext}>Next<i className="glyphicon glyphicon-chevron-right"></i></button> + <button type="button" className="btn btn-default" onClick={this.submitBack}><i className="glyphicon glyphicon-chevron-left"></i> Back</button> + <button type="submit" className="btn-primary btn" onClick={this.submitNext}>Next<i className="glyphicon glyphicon-chevron-right"></i></button> + </form> </div> ); } @@ -531,18 +566,11 @@ PasswordPage = React.createClass({ props.state.wizard = "finished"; props.updateParent(props.state, true); - if (utils.isTestDomain()) { - UserStore.setLastDomain(teamSignup.team.domain); - UserStore.setLastEmail(teamSignup.team.email); - window.location.href = window.location.protocol + '//' + utils.getDomainWithOutSub() + '/login?email=' + encodeURIComponent(teamSignup.team.email); - } - else { - window.location.href = window.location.protocol + '//' + teamSignup.team.domain + '.' + utils.getDomainWithOutSub() + '/login?email=' + encodeURIComponent(teamSignup.team.email); - } + window.location.href = window.location.origin + '/' + props.state.team.name + '/login?email=' + encodeURIComponent(teamSignup.team.email); // client.loginByEmail(teamSignup.team.domain, teamSignup.team.email, teamSignup.user.password, // function(data) { - // UserStore.setLastDomain(teamSignup.team.domain); + // TeamStore.setLastName(teamSignup.team.domain); // UserStore.setLastEmail(teamSignup.team.email); // UserStore.setCurrentUser(data); // window.location.href = '/channels/town-square'; @@ -570,13 +598,14 @@ PasswordPage = React.createClass({ return ( <div> + <form> <img className="signup-team-logo" src="/static/images/logo.png" /> <h2>Choose a password</h2> <p>You'll use your email address ({this.props.state.team.email}) and password to log into {config.SiteName}.</p> <div className={ name_error ? "form-group has-error" : "form-group" }> <div className="row"> <div className="col-sm-9"> - <input type="password" ref="password" className="form-control" placeholder="" maxLength="128" /> + <input autoFocus={true} type="password" ref="password" className="form-control" placeholder="" maxLength="128" /> </div> </div> { name_error } @@ -585,10 +614,11 @@ PasswordPage = React.createClass({ <label><input type="checkbox" ref="email_service" /> It's ok to send me occassional email with updates about the {config.SiteName} service.</label> </div> <div className="form-group"> - <button className="btn btn-default" onClick={this.submitBack}><i className="glyphicon glyphicon-chevron-left"></i> Back</button> - <button className="btn-primary btn" id="finish-button" data-loading-text={"<span class='glyphicon glyphicon-refresh glyphicon-refresh-animate'></span> Creating "+strings.Team+"..."} onClick={this.submitNext}>Finish</button> + <button type="button" className="btn btn-default" onClick={this.submitBack}><i className="glyphicon glyphicon-chevron-left"></i> Back</button> + <button type="submit" className="btn-primary btn" id="finish-button" data-loading-text={"<span class='glyphicon glyphicon-refresh glyphicon-refresh-animate'></span> Creating "+strings.Team+"..."} onClick={this.submitNext}>Finish</button> </div> <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> + </form> </div> ); } @@ -610,9 +640,9 @@ module.exports = React.createClass({ props.wizard = "welcome"; props.team = {}; props.team.email = this.props.email; - props.team.name = this.props.name; + props.team.display_name = this.props.name; props.team.company_name = this.props.name; - props.team.domain = utils.cleanUpUrlable(this.props.name); + props.team.name = utils.cleanUpUrlable(this.props.name); props.team.allowed_domains = ""; props.invites = []; props.invites.push(""); @@ -630,12 +660,12 @@ module.exports = React.createClass({ return <WelcomePage state={this.state} updateParent={this.updateParent} /> } - if (this.state.wizard == "team_name") { - return <TeamNamePage state={this.state} updateParent={this.updateParent} /> + if (this.state.wizard == "team_display_name") { + return <TeamDisplayNamePage state={this.state} updateParent={this.updateParent} /> } if (this.state.wizard == "team_url") { - return <TeamUrlPage state={this.state} updateParent={this.updateParent} /> + return <TeamURLPage state={this.state} updateParent={this.updateParent} /> } if (this.state.wizard == "allowed_domains") { diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index d1053c778..eed323d1f 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -48,16 +48,17 @@ module.exports = React.createClass({ client.loginByEmail(this.props.domain, this.state.user.email, this.state.user.password, function(data) { - UserStore.setLastDomain(this.props.domain); UserStore.setLastEmail(this.state.user.email); UserStore.setCurrentUser(data); if (this.props.hash > 0) + { BrowserStore.setGlobalItem(this.props.hash, JSON.stringify({wizard: "finished"})); - window.location.href = '/channels/town-square'; + } + window.location.href = '/'; }.bind(this), function(err) { if (err.message == "Login failed because email address has not been verified") { - window.location.href = "/verify?email="+ encodeURIComponent(this.state.user.email) + "&domain=" + encodeURIComponent(this.props.domain); + window.location.href = "/verify_email?email="+ encodeURIComponent(this.state.user.email) + "&domain=" + encodeURIComponent(this.props.domain); } else { this.state.server_error = err.message; this.setState(this.state); diff --git a/web/react/components/team_members.jsx b/web/react/components/team_members.jsx index 6b978f88b..616fd2c99 100644 --- a/web/react/components/team_members.jsx +++ b/web/react/components/team_members.jsx @@ -57,7 +57,7 @@ module.exports = React.createClass({ <div className="modal-content"> <div className="modal-header"> <button type="button" className="close" data-dismiss="modal" aria-label="Close" data-reactid=".5.0.0.0.0"><span aria-hidden="true" data-reactid=".5.0.0.0.0.0">×</span></button> - <h4 className="modal-title" id="myModalLabel">{this.props.teamName + " Members"}</h4> + <h4 className="modal-title" id="myModalLabel">{this.props.teamDisplayName + " Members"}</h4> </div> <div ref="modalBody" className="modal-body"> <div className="channel-settings"> diff --git a/web/react/components/user_settings.jsx b/web/react/components/user_settings.jsx index 59c97c309..ad890334e 100644 --- a/web/react/components/user_settings.jsx +++ b/web/react/components/user_settings.jsx @@ -5,6 +5,8 @@ var UserStore = require('../stores/user_store.jsx'); var SettingItemMin = require('./setting_item_min.jsx'); var SettingItemMax = require('./setting_item_max.jsx'); var SettingPicture = require('./setting_picture.jsx'); +var AccessHistoryModal = require('./access_history_modal.jsx'); +var ActivityLogModal = require('./activity_log_modal.jsx'); var client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var utils = require('../utils/utils.jsx'); @@ -443,149 +445,6 @@ var NotificationsTab = React.createClass({ } }); -function getStateFromStoresForSessions() { - return { - sessions: UserStore.getSessions(), - server_error: null, - client_error: null - }; -} - -var SessionsTab = React.createClass({ - submitRevoke: function(altId) { - client.revokeSession(altId, - function(data) { - AsyncClient.getSessions(); - }.bind(this), - function(err) { - state = this.getStateFromStoresForSessions(); - state.server_error = err; - this.setState(state); - }.bind(this) - ); - }, - componentDidMount: function() { - UserStore.addSessionsChangeListener(this._onChange); - AsyncClient.getSessions(); - }, - componentWillUnmount: function() { - UserStore.removeSessionsChangeListener(this._onChange); - }, - _onChange: function() { - this.setState(getStateFromStoresForSessions()); - }, - getInitialState: function() { - return getStateFromStoresForSessions(); - }, - render: function() { - var server_error = this.state.server_error ? this.state.server_error : null; - - return ( - <div> - <div className="modal-header"> - <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> - <h4 className="modal-title" ref="title"><i className="modal-back"></i>Sessions</h4> - </div> - <div className="user-settings"> - <h3 className="tab-header">Sessions</h3> - <div className="divider-dark first"/> - { server_error } - <div className="table-responsive" style={{ maxWidth: "560px", maxHeight: "300px" }}> - <table className="table-condensed small"> - <thead> - <tr><th>Id</th><th>Platform</th><th>OS</th><th>Browser</th><th>Created</th><th>Last Activity</th><th>Revoke</th></tr> - </thead> - <tbody> - { - this.state.sessions.map(function(value, index) { - return ( - <tr key={ "" + index }> - <td style={{ whiteSpace: "nowrap" }}>{ value.alt_id }</td> - <td style={{ whiteSpace: "nowrap" }}>{value.props.platform}</td> - <td style={{ whiteSpace: "nowrap" }}>{value.props.os}</td> - <td style={{ whiteSpace: "nowrap" }}>{value.props.browser}</td> - <td style={{ whiteSpace: "nowrap" }}>{ new Date(value.create_at).toLocaleString() }</td> - <td style={{ whiteSpace: "nowrap" }}>{ new Date(value.last_activity_at).toLocaleString() }</td> - <td><button onClick={this.submitRevoke.bind(this, value.alt_id)} className="pull-right btn btn-primary">Revoke</button></td> - </tr> - ); - }, this) - } - </tbody> - </table> - </div> - <div className="divider-dark"/> - </div> - </div> - ); - } -}); - -function getStateFromStoresForAudits() { - return { - audits: UserStore.getAudits() - }; -} - -var AuditTab = React.createClass({ - componentDidMount: function() { - UserStore.addAuditsChangeListener(this._onChange); - AsyncClient.getAudits(); - }, - componentWillUnmount: function() { - UserStore.removeAuditsChangeListener(this._onChange); - }, - _onChange: function() { - this.setState(getStateFromStoresForAudits()); - }, - getInitialState: function() { - return getStateFromStoresForAudits(); - }, - render: function() { - return ( - <div> - <div className="modal-header"> - <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> - <h4 className="modal-title" ref="title"><i className="modal-back"></i>Activity Log</h4> - </div> - <div className="user-settings"> - <h3 className="tab-header">Activity Log</h3> - <div className="divider-dark first"/> - <div className="table-responsive"> - <table className="table-condensed small"> - <thead> - <tr> - <th>Time</th> - <th>Action</th> - <th>IP Address</th> - <th>Session</th> - <th>Other Info</th> - </tr> - </thead> - <tbody> - { - this.state.audits.map(function(value, index) { - return ( - <tr key={ "" + index }> - <td className="text-nowrap">{ new Date(value.create_at).toLocaleString() }</td> - <td className="text-nowrap">{ value.action.replace("/api/v1", "") }</td> - <td className="text-nowrap">{ value.ip_address }</td> - <td className="text-nowrap">{ value.session_id }</td> - <td className="text-nowrap">{ value.extra_info }</td> - </tr> - ); - }, this) - } - </tbody> - </table> - </div> - <div className="divider-dark"/> - </div> - </div> - ); - } -}); - var SecurityTab = React.createClass({ submitPassword: function(e) { e.preventDefault(); @@ -637,6 +496,12 @@ var SecurityTab = React.createClass({ updateConfirmPassword: function(e) { this.setState({ confirm_password: e.target.value }); }, + handleHistoryOpen: function() { + $("#user_settings1").modal('hide'); + }, + handleDevicesOpen: function() { + $("#user_settings1").modal('hide'); + }, getInitialState: function() { return { current_password: '', new_password: '', confirm_password: '' }; }, @@ -711,6 +576,10 @@ var SecurityTab = React.createClass({ <div className="divider-dark first"/> { passwordSection } <div className="divider-dark"/> + <br></br> + <a data-toggle="modal" className="security-links" data-target="#access-history" href="#" onClick={this.handleHistoryOpen}><i className="fa fa-clock-o"></i>View Access History</a> + <b> </b> + <a data-toggle="modal" className="security-links" data-target="#activity-log" href="#" onClick={this.handleDevicesOpen}><i className="fa fa-globe"></i>View and Logout of Active Devices</a> </div> </div> ); @@ -1225,23 +1094,6 @@ module.exports = React.createClass({ <NotificationsTab user={this.state.user} activeSection={this.props.activeSection} updateSection={this.props.updateSection} /> </div> ); - - /* Temporarily removing sessions and activity_log tabs - - } else if (this.props.activeTab === 'sessions') { - return ( - <div> - <SessionsTab activeSection={this.props.activeSection} updateSection={this.props.updateSection} /> - </div> - ); - } else if (this.props.activeTab === 'activity_log') { - return ( - <div> - <AuditTab activeSection={this.props.activeSection} updateSection={this.props.updateSection} /> - </div> - ); - */ - } else if (this.props.activeTab === 'appearance') { return ( <div> diff --git a/web/react/components/user_settings_modal.jsx b/web/react/components/user_settings_modal.jsx index 1761e575a..421027244 100644 --- a/web/react/components/user_settings_modal.jsx +++ b/web/react/components/user_settings_modal.jsx @@ -30,8 +30,6 @@ module.exports = React.createClass({ tabs.push({name: "security", ui_name: "Security", icon: "glyphicon glyphicon-lock"}); tabs.push({name: "notifications", ui_name: "Notifications", icon: "glyphicon glyphicon-exclamation-sign"}); tabs.push({name: "appearance", ui_name: "Appearance", icon: "glyphicon glyphicon-wrench"}); - //tabs.push({name: "sessions", ui_name: "Sessions", icon: "glyphicon glyphicon-globe"}); - //tabs.push({name: "activity_log", ui_name: "Activity Log", icon: "glyphicon glyphicon-time"}); return ( <div className="modal fade" ref="modal" id="user_settings1" role="dialog" aria-hidden="true"> diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx index 38f439946..c107de4d7 100644 --- a/web/react/components/view_image.jsx +++ b/web/react/components/view_image.jsx @@ -25,13 +25,22 @@ module.exports = React.createClass({ this.setState({ imgId: nextProps.startId }); }, loadImage: function(id) { - if (this.state.loaded[id] || this.state.images[id]) return; + var imgHeight = $(window).height()-100; + if (this.state.loaded[id] || this.state.images[id]){ + $('.modal .modal-image .image-wrapper img').css("max-height",imgHeight); + return; + }; var src = ""; if (this.props.imgCount > 0) { src = this.props.filenames[id]; } else { var fileInfo = utils.splitFileLocation(this.props.filenames[id]); + // This is a temporary patch to fix issue with old files using absolute paths + if (fileInfo.path.indexOf("/api/v1/files/get") !== -1) { + fileInfo.path = fileInfo.path.split("/api/v1/files/get")[1]; + } + fileInfo.path = window.location.origin + "/api/v1/files/get" + fileInfo.path; src = fileInfo['path'] + '_preview.jpg'; } @@ -48,6 +57,7 @@ module.exports = React.createClass({ var loaded = self.state.loaded; loaded[imgid] = true; self.setState({ loaded: loaded }); + $(self.refs.image.getDOMNode()).css("max-height",imgHeight); }; }(id); var images = this.state.images; @@ -56,10 +66,8 @@ module.exports = React.createClass({ }, componentDidUpdate: function() { if (this.refs.image) { - var height = $(window).height()-100; if (this.state.loaded[this.state.imgId]) { $(this.refs.imageWrap.getDOMNode()).removeClass("default"); - $(this.refs.image.getDOMNode()).css("max-height",height); } } }, @@ -136,18 +144,30 @@ module.exports = React.createClass({ if (this.props.imgCount > 0) { preview_filename = this.props.filenames[this.state.imgId]; } else { + // This is a temporary patch to fix issue with old files using absolute paths + if (info.path.indexOf("/api/v1/files/get") !== -1) { + info.path = info.path.split("/api/v1/files/get")[1]; + } + info.path = window.location.origin + "/api/v1/files/get" + info.path; preview_filename = info['path'] + '_preview.jpg'; } var imgClass = "hidden"; if (this.state.loaded[id] && this.state.imgId == id) imgClass = ""; - img[info['path']] = <a key={info['path']} className={imgClass} href={this.props.filenames[id]} target="_blank"><img ref="image" src={preview_filename}/></a>; + img[info['path']] = <a key={info['path']} className={imgClass} href={info.path+"."+info.ext} target="_blank"><img ref="image" src={preview_filename}/></a>; } } var imgFragment = React.addons.createFragment(img); + // This is a temporary patch to fix issue with old files using absolute paths + var download_link = this.props.filenames[this.state.imgId]; + if (download_link.indexOf("/api/v1/files/get") !== -1) { + download_link = download_link.split("/api/v1/files/get")[1]; + } + download_link = window.location.origin + "/api/v1/files/get" + download_link; + return ( <div className="modal fade image_modal" ref="modal" id={this.props.modalId} tabIndex="-1" role="dialog" aria-hidden="true"> <div className="modal-dialog modal-image"> @@ -165,7 +185,7 @@ module.exports = React.createClass({ <span className="text"> | </span> </div> : "" } - <a href={this.props.filenames[id]} download={decodeURIComponent(name)} className="text">Download</a> + <a href={download_link} download={decodeURIComponent(name)} className="text">Download</a> </div> </div> {loading} diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx index 3aa985863..cc78df120 100644 --- a/web/react/pages/channel.jsx +++ b/web/react/pages/channel.jsx @@ -32,6 +32,8 @@ var ErrorBar = require('../components/error_bar.jsx') var ChannelLoader = require('../components/channel_loader.jsx'); var MentionList = require('../components/mention_list.jsx'); var ChannelInfoModal = require('../components/channel_info_modal.jsx'); +var AccessHistoryModal = require('../components/access_history_modal.jsx'); +var ActivityLogModal = require('../components/activity_log_modal.jsx'); var Constants = require('../utils/constants.jsx'); @@ -61,17 +63,17 @@ global.window.setup_channel_page = function(team_name, team_type, team_id, chann ); React.render( - <Navbar teamName={team_name} />, + <Navbar teamDisplayName={team_name} />, document.getElementById('navbar') ); React.render( - <Sidebar teamName={team_name} teamType={team_type} />, + <Sidebar teamDisplayName={team_name} teamType={team_type} />, document.getElementById('sidebar-left') ); React.render( - <RenameTeamModal teamName={team_name} />, + <RenameTeamModal teamDisplayName={team_name} />, document.getElementById('rename_team_modal') ); @@ -91,7 +93,7 @@ global.window.setup_channel_page = function(team_name, team_type, team_id, chann ); React.render( - <TeamMembersModal teamName={team_name} />, + <TeamMembersModal teamDisplayName={team_name} />, document.getElementById('team_members_modal') ); @@ -186,7 +188,7 @@ global.window.setup_channel_page = function(team_name, team_type, team_id, chann ); React.render( - <SidebarRightMenu teamName={team_name} teamType={team_type} />, + <SidebarRightMenu teamDisplayName={team_name} teamType={team_type} />, document.getElementById('sidebar-menu') ); @@ -205,4 +207,14 @@ global.window.setup_channel_page = function(team_name, team_type, team_id, chann document.getElementById('edit_mention_tab') ); + React.render( + <AccessHistoryModal />, + document.getElementById('access_history_modal') + ); + + React.render( + <ActivityLogModal />, + document.getElementById('activity_log_modal') + ); + }; diff --git a/web/react/pages/home.jsx b/web/react/pages/home.jsx index 08dd32f73..b12fa4949 100644 --- a/web/react/pages/home.jsx +++ b/web/react/pages/home.jsx @@ -2,13 +2,14 @@ // See License.txt for license information. var ChannelStore = require('../stores/channel_store.jsx'); +var TeamStore = require('../stores/team_store.jsx'); var Constants = require('../utils/constants.jsx'); -global.window.setup_home_page = function() { +global.window.setup_home_page = function(teamURL) { var last = ChannelStore.getLastVisitedName(); if (last == null || last.length === 0) { - window.location.replace("/channels/" + Constants.DEFAULT_CHANNEL); + window.location = teamURL + "/channels/" + Constants.DEFAULT_CHANNEL; } else { - window.location.replace("/channels/" + last); + window.location = teamURL + "/channels/" + last; } } diff --git a/web/react/pages/login.jsx b/web/react/pages/login.jsx index a4e6b438e..8348f0b5d 100644 --- a/web/react/pages/login.jsx +++ b/web/react/pages/login.jsx @@ -3,9 +3,9 @@ var Login = require('../components/login.jsx'); -global.window.setup_login_page = function() { +global.window.setup_login_page = function(teamDisplayName, teamName) { React.render( - <Login />, + <Login teamDisplayName={teamDisplayName} teamName={teamName}/>, document.getElementById('login') ); }; diff --git a/web/react/pages/password_reset.jsx b/web/react/pages/password_reset.jsx index 6d0d88a10..c7a208973 100644 --- a/web/react/pages/password_reset.jsx +++ b/web/react/pages/password_reset.jsx @@ -3,13 +3,13 @@ var PasswordReset = require('../components/password_reset.jsx'); -global.window.setup_password_reset_page = function(is_reset, team_name, domain, hash, data) { +global.window.setup_password_reset_page = function(is_reset, team_display_name, team_name, hash, data) { React.render( <PasswordReset isReset={is_reset} + teamDisplayName={team_display_name} teamName={team_name} - domain={domain} hash={hash} data={data} />, diff --git a/web/react/pages/signup_team_complete.jsx b/web/react/pages/signup_team_complete.jsx index c17cbdfac..346f2ab5a 100644 --- a/web/react/pages/signup_team_complete.jsx +++ b/web/react/pages/signup_team_complete.jsx @@ -5,7 +5,7 @@ var SignupTeamComplete =require('../components/signup_team_complete.jsx'); global.window.setup_signup_team_complete_page = function(email, name, data, hash) { React.render( - <SignupTeamComplete name={name} email={email} hash={hash} data={data} />, + <SignupTeamComplete name={name} email={email} hash={hash} data={data}/>, document.getElementById('signup-team-complete') ); -};
\ No newline at end of file +}; diff --git a/web/react/stores/browser_store.jsx b/web/react/stores/browser_store.jsx index 4d6eb0b8d..4eed754cc 100644 --- a/web/react/stores/browser_store.jsx +++ b/web/react/stores/browser_store.jsx @@ -8,7 +8,7 @@ function getPrefix() { } // Also change model/utils.go ETAG_ROOT_VERSION -var BROWSER_STORE_VERSION = '.3'; +var BROWSER_STORE_VERSION = '.4'; module.exports = { _initialized: false, diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx index e773bb688..1b3e1a119 100644 --- a/web/react/stores/post_store.jsx +++ b/web/react/stores/post_store.jsx @@ -18,6 +18,7 @@ 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, { @@ -93,6 +94,18 @@ 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(); @@ -151,12 +164,12 @@ var PostStore = assign({}, EventEmitter.prototype, { return BrowserStore.getItem("draft_" + channel_id + "_" + user_id); }, clearDraftUploads: function() { - BrowserStore.actionOnItemsWithPrefix("draft_", function (key, value) { - if (value) { - value.uploadsInProgress = 0; - BrowserStore.setItem(key, value); - } - }); + BrowserStore.actionOnItemsWithPrefix("draft_", function (key, value) { + if (value) { + value.uploadsInProgress = 0; + BrowserStore.setItem(key, value); + } + }); } }); @@ -186,6 +199,9 @@ 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/stores/team_store.jsx b/web/react/stores/team_store.jsx index b7199a4a8..3f12725f8 100644 --- a/web/react/stores/team_store.jsx +++ b/web/react/stores/team_store.jsx @@ -57,10 +57,13 @@ var TeamStore = assign({}, EventEmitter.prototype, { else return null; }, + getCurrentTeamUrl: function() { + return window.location.origin + "/" + this.getCurrent().name; + }, storeTeam: function(team) { - var teams = this._getTeams(); - teams[team.id] = team; - this._storeTeams(teams); + var teams = this._getTeams(); + teams[team.id] = team; + this._storeTeams(teams); }, _storeTeams: function(teams) { BrowserStore.setItem("user_teams", teams); diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx index b0ea719d4..d03016c5d 100644 --- a/web/react/stores/user_store.jsx +++ b/web/react/stores/user_store.jsx @@ -67,9 +67,18 @@ var UserStore = assign({}, EventEmitter.prototype, { }, setCurrentId: function(id) { this._current_id = id; + if (id == null) { + BrowserStore.removeGlobalItem("current_user_id"); + } else { + BrowserStore.setGlobalItem("current_user_id", id); + } }, getCurrentId: function(skipFetch) { - var current_id = this._current_id; + var current_id = this._current_id; + + if (current_id == null) { + current_id = BrowserStore.getGlobalItem("current_user_id"); + } // this is a speical case to force fetch the // current user if it's missing @@ -92,17 +101,11 @@ var UserStore = assign({}, EventEmitter.prototype, { return this._getProfiles()[this.getCurrentId()]; }, setCurrentUser: function(user) { - this.saveProfile(user); this.setCurrentId(user.id); - }, - getLastDomain: function() { - return BrowserStore.getItem("last_domain", ''); - }, - setLastDomain: function(domain) { - BrowserStore.setItem("last_domain", domain); + this.saveProfile(user); }, getLastEmail: function() { - return BrowserStore.getItem("last_email", ''); + return BrowserStore.getItem("last_email", ''); }, setLastEmail: function(email) { BrowserStore.setItem("last_email", email); @@ -144,18 +147,18 @@ var UserStore = assign({}, EventEmitter.prototype, { this._storeProfiles(ps); }, _storeProfiles: function(profiles) { - BrowserStore.setGlobalItem("profiles", profiles); + BrowserStore.setItem("profiles", profiles); var profileUsernameMap = {}; for (var id in profiles) { profileUsernameMap[profiles[id].username] = profiles[id]; } - BrowserStore.setGlobalItem("profileUsernameMap", profileUsernameMap); + BrowserStore.setItem("profileUsernameMap", profileUsernameMap); }, _getProfiles: function() { - return BrowserStore.getGlobalItem("profiles", {}); + return BrowserStore.getItem("profiles", {}); }, _getProfilesUsernameMap: function() { - return BrowserStore.getGlobalItem("profileUsernameMap", {}); + return BrowserStore.getItem("profileUsernameMap", {}); }, setSessions: function(sessions) { BrowserStore.setItem("sessions", sessions); diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index 11d4c2601..1c31dc5ed 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -2,6 +2,7 @@ // See License.txt for license information. var BrowserStore = require('../stores/browser_store.jsx'); +var TeamStore = require('../stores/team_store.jsx'); module.exports.track = function(category, action, label, prop, val) { global.window.snowplow('trackStructEvent', category, action, label, prop, val); @@ -44,7 +45,12 @@ function handleError(method_name, xhr, status, err) { module.exports.track('api', 'api_weberror', method_name, 'message', msg); if (xhr.status == 401) { - window.location.href = '/login?redirect=' + encodeURIComponent(window.location.pathname+window.location.search); + if (window.location.href.indexOf("/channels") === 0) { + window.location.pathname = '/login?redirect=' + encodeURIComponent(window.location.pathname+window.location.search); + } else { + var teamURL = window.location.href.split('/channels')[0]; + window.location.href = teamURL + '/login?redirect=' + encodeURIComponent(window.location.pathname+window.location.search); + } } return e; @@ -205,17 +211,18 @@ module.exports.resetPassword = function(data, success, error) { module.exports.logout = function() { module.exports.track('api', 'api_users_logout'); - BrowserStore.clear(); - window.location.href = "/logout"; + var currentTeamUrl = TeamStore.getCurrentTeamUrl(); + BrowserStore.clear(); + window.location.href = currentTeamUrl + "/logout"; }; -module.exports.loginByEmail = function(domain, email, password, success, error) { +module.exports.loginByEmail = function(name, email, password, success, error) { $.ajax({ url: "/api/v1/users/login", dataType: 'json', contentType: 'application/json', type: 'POST', - data: JSON.stringify({domain: domain, email: email, password: password}), + data: JSON.stringify({name: name, email: email, password: password}), success: function(data, textStatus, xhr) { module.exports.track('api', 'api_users_login_success', data.team_id, 'email', data.email); success(data, textStatus, xhr); @@ -317,7 +324,7 @@ module.exports.inviteMembers = function(data, success, error) { module.exports.track('api', 'api_teams_invite_members'); }; -module.exports.updateTeamName = function(data, success, error) { +module.exports.updateTeamDisplayName = function(data, success, error) { $.ajax({ url: "/api/v1/teams/update_name", dataType: 'json', @@ -326,7 +333,7 @@ module.exports.updateTeamName = function(data, success, error) { data: JSON.stringify(data), success: success, error: function(xhr, status, err) { - e = handleError("updateTeamName", xhr, status, err); + e = handleError("updateTeamDisplayName", xhr, status, err); error(e); } }); @@ -334,13 +341,13 @@ module.exports.updateTeamName = function(data, success, error) { module.exports.track('api', 'api_teams_update_name'); }; -module.exports.signupTeam = function(email, name, success, error) { +module.exports.signupTeam = function(email, display_name, success, error) { $.ajax({ url: "/api/v1/teams/signup", dataType: 'json', contentType: 'application/json', type: 'POST', - data: JSON.stringify({email: email, name: name}), + data: JSON.stringify({email: email, display_name: display_name}), success: success, error: function(xhr, status, err) { e = handleError("singupTeam", xhr, status, err); @@ -366,16 +373,16 @@ module.exports.createTeam = function(team, success, error) { }); }; -module.exports.findTeamByDomain = function(domain, success, error) { +module.exports.findTeamByName = function(teamName, success, error) { $.ajax({ - url: "/api/v1/teams/find_team_by_domain", + url: "/api/v1/teams/find_team_by_name", dataType: 'json', contentType: 'application/json', type: 'POST', - data: JSON.stringify({domain: domain}), + data: JSON.stringify({name: teamName}), success: success, error: function(xhr, status, err) { - e = handleError("findTeamByDomain", xhr, status, err); + e = handleError("findTeamByName", xhr, status, err); error(e); } }); diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 3aadfb4b0..2249da0d3 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -18,6 +18,7 @@ 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, @@ -54,7 +55,7 @@ module.exports = { DEFAULT_CHANNEL: 'town-square', OFFTOPIC_CHANNEL: 'off-topic', POST_CHUNK_SIZE: 60, - RESERVED_DOMAINS: [ + RESERVED_TEAM_NAMES: [ "www", "web", "admin", diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 416ea5ae4..00580af6e 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -16,10 +16,10 @@ module.exports.isEmail = function(email) { }; module.exports.cleanUpUrlable = function(input) { - var cleaned = input.trim().replace(/-/g, ' ').replace(/[^\w\s]/gi, '').toLowerCase().replace(/\s/g, '-'); - cleaned = cleaned.replace(/^\-+/, ''); - cleaned = cleaned.replace(/\-+$/, ''); - return cleaned; + var cleaned = input.trim().replace(/-/g, ' ').replace(/[^\w\s]/gi, '').toLowerCase().replace(/\s/g, '-'); + cleaned = cleaned.replace(/^\-+/, ''); + cleaned = cleaned.replace(/\-+$/, ''); + return cleaned; }; @@ -114,7 +114,7 @@ module.exports.notifyMe = function(title, body, channel) { if (channel) { module.exports.switchChannel(channel); } else { - window.location.href = "/channels/town-square"; + window.location.href = "/"; } }; setTimeout(function(){ @@ -708,8 +708,8 @@ module.exports.switchChannel = function(channel, teammate_name) { id: channel.id }); - var domain = window.location.href.split('/channels')[0]; - history.replaceState('data', '', domain + '/channels/' + channel.name); + var teamURL = window.location.href.split('/channels')[0]; + history.replaceState('data', '', teamURL + '/channels/' + channel.name); if (channel.type === 'D' && teammate_name) { document.title = teammate_name + " " + document.title.substring(document.title.lastIndexOf("-")); @@ -784,18 +784,6 @@ Image.prototype.load = function(url, progressCallback) { Image.prototype.completedPercentage = 0; -module.exports.getHomeLink = function() { - if (config.HomeLink != "") { - return config.HomeLink; - } - var parts = window.location.host.split("."); - if (parts.length <= 1) { - return window.location.protocol + "//" + window.location.host; - } - parts[0] = "www"; - return window.location.protocol + "//" + parts.join("."); -} - module.exports.changeColor =function(col, amt) { var usePound = false; diff --git a/web/sass-files/sass/partials/_access-history.scss b/web/sass-files/sass/partials/_access-history.scss new file mode 100644 index 000000000..f54c9a122 --- /dev/null +++ b/web/sass-files/sass/partials/_access-history.scss @@ -0,0 +1,29 @@ +.access-history__table { + display: table; + width: 100%; + padding-top: 15px; + line-height: 1.6; + &:first-child { + padding: 0; + } + > div { + display: table-cell; + vertical-align: top; + } + .access__date { + font-weight: 600; + font-size: 16px; + width: 190px; + } + .access__report { + border-bottom: 1px solid #ddd; + padding-bottom: 15px; + } + .report__time { + font-weight: 600; + font-size: 16px; + } + .report__info { + color: #999; + } +}
\ No newline at end of file diff --git a/web/sass-files/sass/partials/_activity-log.scss b/web/sass-files/sass/partials/_activity-log.scss new file mode 100644 index 000000000..36eb48750 --- /dev/null +++ b/web/sass-files/sass/partials/_activity-log.scss @@ -0,0 +1,31 @@ +.activity-log__table { + display: table; + width: 100%; + line-height: 1.8; + border-top: 1px solid #DDD; + padding: 15px 0; + &:first-child { + padding-top: 0; + border: none; + } + > div { + display: table-cell; + vertical-align: top; + } + .activity-log__report { + width: 80%; + } + .activity-log__action { + text-align: right; + } + .report__platform { + font-size: 16px; + font-weight: 600; + .fa { + margin-right: 6px; + } + } + .report__info { + color: #999; + } +}
\ No newline at end of file diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss index c2740891a..eab4becac 100644 --- a/web/sass-files/sass/partials/_headers.scss +++ b/web/sass-files/sass/partials/_headers.scss @@ -44,14 +44,16 @@ white-space: normal; } } + } .channel-intro { padding-bottom:5px; margin: 0 1em 35px; + max-width: 850px; border-bottom: 1px solid lightgrey; .intro-links { - margin: 0.5em 1.5em 0 0; + margin: 0 1.5em 10px 0; display: inline-block; .fa { margin-right: 5px; @@ -64,8 +66,15 @@ .channel-intro-img { float:left; } - .channel-intro-title { + .channel-intro__title { font-weight:600; + font-size: 20px; + margin-bottom: 15px; + } + .channel-intro__content { + background: #f7f7f7; + padding: 10px 15px; + @include border-radius(3px); } .channel-intro-text { margin-top:35px; @@ -98,14 +107,17 @@ fill: #fff; } } + .settings__link a:hover, a:visited, a:link, a:active { + text-decoration: none; + } .user__picture { width: 36px; height: 36px; float: left; @include border-radius(36px); + margin-right: 6px; } .header__info { - padding-left: 42px; color: #fff; } .team__name, .user__name { @@ -248,4 +260,4 @@ margin-top: 8px; fill: #AAA; } -}
\ No newline at end of file +} diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index 9368786d1..df565d763 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -187,6 +187,12 @@ body.ios { .post-create-footer { @include clearfix; padding: 0; + .has-error { + .control-label { + font-weight: normal; + margin-bottom: 0; + } + } .msg-typing { min-height: 20px; line-height: 18px; @@ -209,9 +215,6 @@ body.ios { @include opacity(1); } .dropdown-toggle:after { - content: '...'; - } - .dropdown-toggle:hover:after { content: '[...]'; } } @@ -316,6 +319,12 @@ 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; @@ -431,4 +440,4 @@ body.ios { width: 40px; } } -}
\ No newline at end of file +} diff --git a/web/sass-files/sass/partials/_post_right.scss b/web/sass-files/sass/partials/_post_right.scss index 8816393c8..4cf3e32a1 100644 --- a/web/sass-files/sass/partials/_post_right.scss +++ b/web/sass-files/sass/partials/_post_right.scss @@ -11,7 +11,7 @@ .post { &.post--root { - padding: 1em 1em 0; + padding: 0 1em 0; margin: 1em 0; hr { border-color: #DDD; @@ -62,6 +62,11 @@ } } +.post-right-channel__name { + font-weight: 600; + margin: 0 0 10px 0; +} + .post-right-root-container li { display: inline; list-style-type: none; diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index a33d69378..d8a8fd982 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -89,6 +89,9 @@ max-width: 810px; } } + .channel-intro { + max-width: 810px; + } .date-separator, .new-separator { &.hovered--comment { &:before, &:after { @@ -214,6 +217,12 @@ } } +@media (min-width: 992px){ + .modal-lg { + width: 700px; + } +} + @media screen and (min-width: 768px) { .second-bar { display: none; @@ -239,6 +248,11 @@ } &:hover { background: none; + .post-header .post-header-col.post-header__reply { + .dropdown-toggle:after { + content: '...'; + } + } } &.post--comment { &.other--root { @@ -247,6 +261,11 @@ } } } + .post-header .post-header-col.post-header__reply { + .dropdown-toggle:after { + content: '...'; + } + } } .signup-team__container { padding: 30px 0; @@ -630,6 +649,33 @@ padding: 9px 21px 10px 10px !important; } } +@media screen and (max-width: 640px) { + .access-history__table { + > div { + display: block; + } + .access__report { + margin: 0 0 15px 15px; + } + .access__date { + div { + margin-bottom: 15px; + } + } + } + .activity-log__table { + > div { + display: block; + } + .activity-log__report { + width: 100%; + } + .activity-log__action { + text-align: left; + margin-top: 10px; + } + } +} @media screen and (max-width: 480px) { .modal { .modal-body { diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss index e60bc290e..b8dc9e997 100644 --- a/web/sass-files/sass/partials/_settings.scss +++ b/web/sass-files/sass/partials/_settings.scss @@ -1,3 +1,6 @@ +@import "access-history"; +@import "activity-log"; + .user-settings { background: #fff; min-height:300px; @@ -32,6 +35,12 @@ display: table-cell; vertical-align: top; } + .security-links { + margin-right: 20px; + .fa { + margin-right: 6px; + } + } .settings-links { width: 180px; background: #FAFAFA; @@ -223,4 +232,4 @@ .color-btn { margin:4px; -} +}
\ No newline at end of file diff --git a/web/sass-files/sass/partials/_signup.scss b/web/sass-files/sass/partials/_signup.scss index a714aa44f..98931279b 100644 --- a/web/sass-files/sass/partials/_signup.scss +++ b/web/sass-files/sass/partials/_signup.scss @@ -6,7 +6,7 @@ } .signup-team__container { padding: 100px 0px 50px 0px; - max-width: 340px; + max-width: 600px; margin: 0 auto; font-size: 1.1em; position: relative; @@ -118,4 +118,4 @@ .signup-team-login { padding-bottom: 10px; font-weight: 700; -}
\ No newline at end of file +} diff --git a/web/static/help/configure_links.html b/web/static/help/configure_links.html index be6490192..1c564e0d6 100644 --- a/web/static/help/configure_links.html +++ b/web/static/help/configure_links.html @@ -15,9 +15,14 @@ Learn more, or download the source code from <a href=http://mattermost.com>http: <p>To take part in the community building Mattermost, please consider sharing comments, feature requests, votes, and contributions. If you like the project, please Tweet about us at <a href=https://twitter.com/mattermosthq>@mattermosthq</a>.</p> <p>Here's some links to get started:<br> -<li><a href=http://bit.ly/1dHmQqX>Mattermost source code and install instructions</a></li> -<li><a href=http://bit.ly/1JUDoZ3>Mattermost Feature Request and Voting Site</a> </li> -<li><a href=http://bit.ly/1MH9HKa>Mattermost Issue Tracker for reporting bugs</a></li> +<ul> + <li><a href="https://github.com/mattermost/platform">Follow Mattermost on Github</a></li> + <li><a href="http://forum.mattermost.org/">Ask us anything at http://forum.mattermost.org/</a></li> + <li><a href="http://www.mattermost.org/feature-requests/">Review the Mattermost feature list </a></li> + <li><a href="http://www.mattermost.org/download/">Download our source code and install instructions</a></li> + <li><a href="http://www.mattermost.org/feature-requests/">Share feature requests and upvotes</a></li> + <li><a href="http://www.mattermost.org/filing-issues/">File any bugs you find with our Issue tracking system</a></li> +</ul> </p> </body> </html> diff --git a/web/templates/channel.html b/web/templates/channel.html index d96aee3d4..8e856032d 100644 --- a/web/templates/channel.html +++ b/web/templates/channel.html @@ -45,8 +45,10 @@ <div id="team_members_modal"></div> <div id="direct_channel_modal"></div> <div id="channel_info_modal"></div> + <div id="access_history_modal"></div> + <div id="activity_log_modal"></div> <script> -window.setup_channel_page('{{ .Props.TeamName }}', '{{ .Props.TeamType }}', '{{ .Props.TeamId }}', '{{ .Props.ChannelName }}', '{{ .Props.ChannelId }}'); +window.setup_channel_page('{{ .Props.TeamDisplayName }}', '{{ .Props.TeamType }}', '{{ .Props.TeamId }}', '{{ .Props.ChannelName }}', '{{ .Props.ChannelId }}'); </script> </body> </html> diff --git a/web/templates/home.html b/web/templates/home.html index abf8062f2..9ec8b7000 100644 --- a/web/templates/home.html +++ b/web/templates/home.html @@ -17,7 +17,7 @@ </div> </div> <script> - window.setup_home_page(); + window.setup_home_page({{.Props.TeamURL}}); </script> </body> </html> diff --git a/web/templates/login.html b/web/templates/login.html index c107e1ad5..24cebec8f 100644 --- a/web/templates/login.html +++ b/web/templates/login.html @@ -7,7 +7,7 @@ <div class="inner__wrap"> <div class="row content"> <div class="signup-header"> - {{.Props.TeamName}} + {{.Props.TeamDisplayName}} </div> <div class="col-sm-12"> <div id="login"></div> @@ -20,7 +20,7 @@ </div> </div> <script> - window.setup_login_page(); +window.setup_login_page({{.Props.TeamDisplayName}}, {{.Props.TeamName}}); </script> </body> </html> diff --git a/web/templates/password_reset.html b/web/templates/password_reset.html index 8b63556b1..6244f6418 100644 --- a/web/templates/password_reset.html +++ b/web/templates/password_reset.html @@ -9,7 +9,7 @@ </div> </div> <script> - window.setup_password_reset_page('{{ .Props.IsReset }}', '{{ .Props.TeamName }}', '{{ .Props.Domain }}', '{{ .Props.Hash }}', '{{ .Props.Data }}'); + window.setup_password_reset_page('{{ .Props.IsReset }}', '{{ .Props.TeamDisplayName }}', '{{ .Props.TeamName }}', '{{ .Props.Hash }}', '{{ .Props.Data }}'); </script> </body> </html> diff --git a/web/templates/signup_team.html b/web/templates/signup_team.html index fad332bee..b86590589 100644 --- a/web/templates/signup_team.html +++ b/web/templates/signup_team.html @@ -12,7 +12,6 @@ <h2>All team communication in one place, searchable and accessible anywhere</h2> <h4 class="text--light">{{ .SiteName }} is free for an unlimited time, for unlimited users </h4 class="text--light"> <div id="signup-team"></div> - <a class="signup-team-login" href="/login">or Sign In</a> </div> </div> <div class="footer-push"></div> @@ -23,7 +22,7 @@ </div> </div> <script> - window.setup_signup_team_page(); +window.setup_signup_team_page(); </script> </body> </html> diff --git a/web/templates/signup_team_complete.html b/web/templates/signup_team_complete.html index 59f49cdbd..674e54ee2 100644 --- a/web/templates/signup_team_complete.html +++ b/web/templates/signup_team_complete.html @@ -19,7 +19,7 @@ </div> </div> <script> - window.setup_signup_team_complete_page('{{.Props.Email}}', '{{.Props.Name}}', '{{.Props.Data}}', '{{.Props.Hash}}'); +window.setup_signup_team_complete_page('{{.Props.Email}}', '{{.Props.DisplayName}}', '{{.Props.Data}}', '{{.Props.Hash}}'); </script> </body> </html> diff --git a/web/templates/signup_team_confirm.html b/web/templates/signup_team_confirm.html index 9e21126da..2d27194bc 100644 --- a/web/templates/signup_team_confirm.html +++ b/web/templates/signup_team_confirm.html @@ -8,10 +8,9 @@ <div class="row content"> <div class="col-sm-12"> <div class="signup-team__container"> - <p>Did you mean to sign-in rather than sign up? Sign in <a href="/login">here</a>. </p> <h3>Sign up Complete</h3> <p>Please check your email: {{ .Props.Email }}<br> - You email contains a link to set up your team</p> + Your email contains a link to set up your team</p> </div> </div> </div> diff --git a/web/templates/signup_user_complete.html b/web/templates/signup_user_complete.html index 5fe907ba7..0cc655b63 100644 --- a/web/templates/signup_user_complete.html +++ b/web/templates/signup_user_complete.html @@ -19,7 +19,7 @@ </div> </div> <script> - window.setup_signup_user_complete_page('{{.Props.Email}}', '{{.Props.TeamDomain}}', '{{.Props.TeamName}}', '{{.Props.TeamId}}', '{{.Props.Data}}', '{{.Props.Hash}}'); + window.setup_signup_user_complete_page('{{.Props.Email}}', '{{.Props.TeamName}}', '{{.Props.TeamDisplayName}}', '{{.Props.TeamId}}', '{{.Props.Data}}', '{{.Props.Hash}}'); </script> </body> </html> diff --git a/web/web.go b/web/web.go index b11e6e6b1..3e4bc2d53 100644 --- a/web/web.go +++ b/web/web.go @@ -42,25 +42,28 @@ func (me *HtmlTemplatePage) Render(c *api.Context, w http.ResponseWriter) { func InitWeb() { l4g.Debug("Initializing web routes") + mainrouter := api.Srv.Router + staticDir := utils.FindDir("web/static") l4g.Debug("Using static directory at %v", staticDir) - api.Srv.Router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", - http.FileServer(http.Dir(staticDir)))) - - api.Srv.Router.Handle("/", api.AppHandler(root)).Methods("GET") - api.Srv.Router.Handle("/login", api.AppHandler(login)).Methods("GET") - api.Srv.Router.Handle("/signup_team_confirm/", api.AppHandler(signupTeamConfirm)).Methods("GET") - api.Srv.Router.Handle("/signup_team_complete/", api.AppHandler(signupTeamComplete)).Methods("GET") - api.Srv.Router.Handle("/signup_user_complete/", api.AppHandler(signupUserComplete)).Methods("GET") - - api.Srv.Router.Handle("/logout", api.AppHandler(logout)).Methods("GET") - - api.Srv.Router.Handle("/verify", api.AppHandler(verifyEmail)).Methods("GET") - api.Srv.Router.Handle("/find_team", api.AppHandler(findTeam)).Methods("GET") - api.Srv.Router.Handle("/reset_password", api.AppHandler(resetPassword)).Methods("GET") - - csr := api.Srv.Router.PathPrefix("/channels").Subrouter() - csr.Handle("/{name:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}", api.UserRequired(getChannel)).Methods("GET") + mainrouter.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))) + + mainrouter.Handle("/", api.AppHandlerIndependent(root)).Methods("GET") + mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}", api.AppHandler(login)).Methods("GET") + mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/", api.AppHandler(login)).Methods("GET") + mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/login", api.AppHandler(login)).Methods("GET") + mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/logout", api.AppHandler(logout)).Methods("GET") + mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/reset_password", api.AppHandler(resetPassword)).Methods("GET") + // Bug in gorilla.mux pervents us from using regex here. + mainrouter.Handle("/{team}/channels/{channelname}", api.UserRequired(getChannel)).Methods("GET") + + // Anything added here must have an _ in it so it does not conflict with team names + mainrouter.Handle("/signup_team_complete/", api.AppHandlerIndependent(signupTeamComplete)).Methods("GET") + mainrouter.Handle("/signup_user_complete/", api.AppHandlerIndependent(signupUserComplete)).Methods("GET") + mainrouter.Handle("/signup_team_confirm/", api.AppHandlerIndependent(signupTeamConfirm)).Methods("GET") + mainrouter.Handle("/verify_email", api.AppHandlerIndependent(verifyEmail)).Methods("GET") + mainrouter.Handle("/find_team", api.AppHandlerIndependent(findTeam)).Methods("GET") + mainrouter.Handle("/signup_team", api.AppHandlerIndependent(signup)).Methods("GET") watchAndParseTemplates() } @@ -128,46 +131,53 @@ func root(c *api.Context, w http.ResponseWriter, r *http.Request) { } if len(c.Session.UserId) == 0 { - if api.IsTestDomain(r) || strings.Index(r.Host, "www") == 0 || strings.Index(r.Host, "beta") == 0 || strings.Index(r.Host, "ci") == 0 { - page := NewHtmlTemplatePage("signup_team", "Signup") - page.Render(c, w) - } else { - login(c, w, r) - } + page := NewHtmlTemplatePage("signup_team", "Signup") + page.Render(c, w) } else { page := NewHtmlTemplatePage("home", "Home") + page.Props["TeamURL"] = c.GetTeamURL() page.Render(c, w) } } -func login(c *api.Context, w http.ResponseWriter, r *http.Request) { +func signup(c *api.Context, w http.ResponseWriter, r *http.Request) { + if !CheckBrowserCompatability(c, r) { return } - teamName := "Beta" - teamDomain := "" - siteDomain := "." + utils.Cfg.ServiceSettings.Domain + page := NewHtmlTemplatePage("signup_team", "Signup") + page.Render(c, w) +} + +func login(c *api.Context, w http.ResponseWriter, r *http.Request) { + if !CheckBrowserCompatability(c, r) { + return + } + params := mux.Vars(r) + teamName := params["team"] - if utils.Cfg.ServiceSettings.Mode == utils.MODE_DEV { - teamDomain = "developer" - } else if utils.Cfg.ServiceSettings.Mode == utils.MODE_BETA { - teamDomain = "beta" + var team *model.Team + if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil { + l4g.Error("Couldn't find team name=%v, teamURL=%v, err=%v", teamName, c.GetTeamURL(), tResult.Err.Message) + // This should probably do somthing nicer + http.Redirect(w, r, "http://"+r.Host, http.StatusTemporaryRedirect) + return } else { - teamDomain, siteDomain = model.GetSubDomain(c.TeamUrl) - siteDomain = "." + siteDomain + ".com" + team = tResult.Data.(*model.Team) + } - if tResult := <-api.Srv.Store.Team().GetByDomain(teamDomain); tResult.Err != nil { - l4g.Error("Couldn't find team teamDomain=%v, siteDomain=%v, teamUrl=%v, err=%v", teamDomain, siteDomain, c.TeamUrl, tResult.Err.Message) - } else { - teamName = tResult.Data.(*model.Team).Name - } + // If we are already logged into this team then go to home + if len(c.Session.UserId) != 0 && c.Session.TeamId == team.Id { + page := NewHtmlTemplatePage("home", "Home") + page.Props["TeamURL"] = c.GetTeamURL() + page.Render(c, w) + return } page := NewHtmlTemplatePage("login", "Login") + page.Props["TeamDisplayName"] = team.DisplayName page.Props["TeamName"] = teamName - page.Props["TeamDomain"] = teamDomain - page.Props["SiteDomain"] = siteDomain page.Render(c, w) } @@ -198,7 +208,7 @@ func signupTeamComplete(c *api.Context, w http.ResponseWriter, r *http.Request) page := NewHtmlTemplatePage("signup_team_complete", "Complete Team Sign Up") page.Props["Email"] = props["email"] - page.Props["Name"] = props["name"] + page.Props["DisplayName"] = props["display_name"] page.Props["Data"] = data page.Props["Hash"] = hash page.Render(c, w) @@ -225,8 +235,8 @@ func signupUserComplete(c *api.Context, w http.ResponseWriter, r *http.Request) } props["email"] = "" + props["display_name"] = team.DisplayName props["name"] = team.Name - props["domain"] = team.Domain props["id"] = team.Id data = model.MapToJson(props) hash = "" @@ -249,8 +259,8 @@ func signupUserComplete(c *api.Context, w http.ResponseWriter, r *http.Request) page := NewHtmlTemplatePage("signup_user_complete", "Complete User Sign Up") page.Props["Email"] = props["email"] + page.Props["TeamDisplayName"] = props["display_name"] page.Props["TeamName"] = props["name"] - page.Props["TeamDomain"] = props["domain"] page.Props["TeamId"] = props["id"] page.Props["Data"] = data page.Props["Hash"] = hash @@ -259,12 +269,12 @@ func signupUserComplete(c *api.Context, w http.ResponseWriter, r *http.Request) func logout(c *api.Context, w http.ResponseWriter, r *http.Request) { api.Logout(c, w, r) - http.Redirect(w, r, "/", http.StatusFound) + http.Redirect(w, r, c.GetTeamURL(), http.StatusFound) } func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - name := params["name"] + name := params["channelname"] var channelId string if result := <-api.Srv.Store.Channel().CheckPermissionsToByName(c.Session.TeamId, name, c.Session.UserId); result.Err != nil { @@ -304,7 +314,7 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { //api.Handle404(w, r) //Bad channel urls just redirect to the town-square for now - http.Redirect(w, r, "/channels/town-square", http.StatusFound) + http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound) return } } @@ -319,8 +329,8 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { } page := NewHtmlTemplatePage("channel", "") - page.Title = name + " - " + team.Name + " " + page.SiteName - page.Props["TeamName"] = team.Name + page.Title = name + " - " + team.DisplayName + " " + page.SiteName + page.Props["TeamDisplayName"] = team.DisplayName page.Props["TeamType"] = team.Type page.Props["TeamId"] = team.Id page.Props["ChannelName"] = name @@ -331,7 +341,7 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) { resend := r.URL.Query().Get("resend") - domain := r.URL.Query().Get("domain") + name := r.URL.Query().Get("name") email := r.URL.Query().Get("email") hashedId := r.URL.Query().Get("hid") userId := r.URL.Query().Get("uid") @@ -339,7 +349,7 @@ func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) { if resend == "true" { teamId := "" - if result := <-api.Srv.Store.Team().GetByDomain(domain); result.Err != nil { + if result := <-api.Srv.Store.Team().GetByName(name); result.Err != nil { c.Err = result.Err return } else { @@ -351,7 +361,7 @@ func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) { return } else { user := result.Data.(*model.User) - api.FireAndForgetVerifyEmail(user.Id, user.FirstName, user.Email, domain, c.TeamUrl) + api.FireAndForgetVerifyEmail(user.Id, strings.Split(user.Nickname, " ")[0], user.Email, name, c.GetTeamURL()) http.Redirect(w, r, "/", http.StatusFound) return } @@ -387,6 +397,8 @@ func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) { isResetLink := true hash := r.URL.Query().Get("h") data := r.URL.Query().Get("d") + params := mux.Vars(r) + teamName := params["team"] if len(hash) == 0 || len(data) == 0 { isResetLink = false @@ -405,30 +417,25 @@ func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) { } } - teamName := "Developer/Beta" - domain := "" - if utils.Cfg.ServiceSettings.Mode != utils.MODE_DEV { - domain, _ = model.GetSubDomain(c.TeamUrl) - - var team *model.Team - if tResult := <-api.Srv.Store.Team().GetByDomain(domain); tResult.Err != nil { - c.Err = tResult.Err - return - } else { - team = tResult.Data.(*model.Team) - } + teamDisplayName := "Developer/Beta" + var team *model.Team + if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil { + c.Err = tResult.Err + return + } else { + team = tResult.Data.(*model.Team) + } - if team != nil { - teamName = team.Name - } + if team != nil { + teamDisplayName = team.DisplayName } page := NewHtmlTemplatePage("password_reset", "") page.Title = "Reset Password - " + page.SiteName - page.Props["TeamName"] = teamName + page.Props["TeamDisplayName"] = teamDisplayName page.Props["Hash"] = hash page.Props["Data"] = data - page.Props["Domain"] = domain + page.Props["TeamName"] = teamName page.Props["IsReset"] = strconv.FormatBool(isResetLink) page.Render(c, w) } |