diff options
Diffstat (limited to 'web/react')
53 files changed, 1079 insertions, 1336 deletions
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx index 68de80228..30435dc08 100644 --- a/web/react/components/channel_header.jsx +++ b/web/react/components/channel_header.jsx @@ -1,6 +1,7 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. + var ChannelStore = require('../stores/channel_store.jsx'); var UserStore = require('../stores/user_store.jsx'); var PostStore = require('../stores/post_store.jsx'); @@ -16,7 +17,7 @@ var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; -var ExtraMembers = React.createClass({ +var PopoverListMembers = React.createClass({ componentDidMount: function() { var originalLeave = $.fn.popover.Constructor.prototype.leave; $.fn.popover.Constructor.prototype.leave = function(obj) { @@ -35,30 +36,29 @@ var ExtraMembers = React.createClass({ $("#member_popover").popover({placement : 'bottom', trigger: 'click', html: true}); $('body').on('click', function (e) { - if ($(e.target.parentNode.parentNode)[0] !== $("#member_popover")[0] && $(e.target).parents('.popover.in').length === 0) { + if ($(e.target.parentNode.parentNode)[0] !== $("#member_popover")[0] && $(e.target).parents('.popover.in').length === 0) { $("#member_popover").popover('hide'); } }); - }, + render: function() { - var count = this.props.members.length == 0 ? "-" : this.props.members.length; - count = this.props.members.length > 19 ? "20+" : count; - var data_content = ""; - var sortedMembers = this.props.members; + var popoverHtml = ''; + var members = this.props.members; + var count = (members.length > 20) ? "20+" : (members.length || '-'); - if(sortedMembers) { - sortedMembers.sort(function(a,b) { + if (members) { + members.sort(function(a,b) { return a.username.localeCompare(b.username); - }) + }); - sortedMembers.forEach(function(m) { - data_content += "<div style='white-space: nowrap'>" + m.username + "</div>"; + members.forEach(function(m) { + popoverHtml += "<div class='text--nowrap'>" + m.username + "</div>"; }); } return ( - <div style={{"cursor" : "pointer"}} id="member_popover" data-toggle="popover" data-content={data_content} data-original-title="Members" > + <div id="member_popover" data-toggle="popover" data-content={popoverHtml} data-original-title="Members" > <div id="member_tooltip" data-toggle="tooltip" title="View Channel Members"> {count} <span className="glyphicon glyphicon-user" aria-hidden="true"></span> </div> @@ -78,6 +78,7 @@ function getStateFromStores() { } module.exports = React.createClass({ + displayName: 'ChannelHeader', componentDidMount: function() { ChannelStore.addChangeListener(this._onChange); ChannelStore.addExtraInfoChangeListener(this._onChange); @@ -99,7 +100,7 @@ module.exports = React.createClass({ $(".channel-header__info .description").popover({placement : 'bottom', trigger: 'hover', html: true, delay: {show: 500, hide: 500}}); }, _onSocketChange: function(msg) { - if(msg.action === "new_user") { + if (msg.action === "new_user") { AsyncClient.getChannelExtraInfo(true); } }, @@ -107,15 +108,14 @@ module.exports = React.createClass({ return getStateFromStores(); }, handleLeave: function(e) { - var self = this; Client.leaveChannel(this.state.channel.id, function(data) { var townsquare = ChannelStore.getByName('town-square'); utils.switchChannel(townsquare); - }.bind(this), + }, function(err) { AsyncClient.dispatchError(err, "handleLeave"); - }.bind(this) + } ); }, searchMentions: function(e) { @@ -131,52 +131,29 @@ module.exports = React.createClass({ AppDispatcher.handleServerAction({ type: ActionTypes.RECIEVED_SEARCH_TERM, term: terms, - do_search: false + do_search: true, + is_mention_search: true }); - - Client.search( - terms, - function(data) { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECIEVED_SEARCH, - results: data, - is_mention_search: true - }); - }, - function(err) { - dispatchError(err, "search"); - } - ); }, + render: function() { if (this.state.channel == null) { - return ( - <div></div> - ); + return null; } - var description = utils.textToJsx(this.state.channel.description, {"singleline": true, "noMentionHighlight": true}); - var popoverContent = React.renderToString(<MessageWrapper message={this.state.channel.description}/>); - var channelTitle = ""; - var channelName = this.state.channel.name; + var channel = this.state.channel; + var description = utils.textToJsx(channel.description, {"singleline": true, "noMentionHighlight": true}); + var popoverContent = React.renderToString(<MessageWrapper message={channel.description}/>); + var channelTitle = channel.display_name; var currentId = UserStore.getCurrentId(); var isAdmin = this.state.memberChannel.roles.indexOf("admin") > -1 || this.state.memberTeam.roles.indexOf("admin") > -1; - var searchForm = <th className="search-bar__container"><NavbarSearchBox /></th>; - var isDirect = false; - - if (this.state.channel.type === 'O') { - channelTitle = this.state.channel.display_name; - } else if (this.state.channel.type === 'P') { - channelTitle = this.state.channel.display_name; - } else if (this.state.channel.type === 'D') { - isDirect = true; + var isDirect = (this.state.channel.type === 'D'); + + if (isDirect) { if (this.state.users.length > 1) { - if (this.state.users[0].id === UserStore.getCurrentId()) { - channelTitle = <UserProfile userId={this.state.users[1].id} overwriteName={this.state.users[1].full_name ? this.state.users[1].full_name : this.state.users[1].username} />; - } else { - channelTitle = <UserProfile userId={this.state.users[0].id} overwriteName={this.state.users[0].full_name ? this.state.users[0].full_name : this.state.users[0].username} />; - } + var contact = this.state.users[((this.state.users[0].id === currentId) ? 1 : 0)]; + channelTitle = <UserProfile userId={contact.id} overwriteName={contact.nickname || contact.username} />; } } @@ -192,25 +169,28 @@ module.exports = React.createClass({ <span className="glyphicon glyphicon-chevron-down header-dropdown__icon"></span> </a> <ul className="dropdown-menu" role="menu" aria-labelledby="channel_header_dropdown"> - <li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_info" data-channelid={this.state.channel.id} href="#">View Info</a></li> - <li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_invite" href="#">Add Members</a></li> - { isAdmin ? + <li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_info" data-channelid={channel.id} href="#">View Info</a></li> + { !ChannelStore.isDefault(channel) ? + <li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_invite" href="#">Add Members</a></li> + : null + } + { isAdmin && !ChannelStore.isDefault(channel) ? <li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_members" href="#">Manage Members</a></li> - : "" + : null } - <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#edit_channel" data-desc={this.state.channel.description} data-title={this.state.channel.display_name} data-channelid={this.state.channel.id}>Set Channel Description...</a></li> - <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#channel_notifications" data-title={this.state.channel.display_name} data-channelid={this.state.channel.id}>Notification Preferences</a></li> - { isAdmin && channelName != Constants.DEFAULT_CHANNEL ? - <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#rename_channel" data-display={this.state.channel.display_name} data-name={this.state.channel.name} data-channelid={this.state.channel.id}>Rename Channel...</a></li> - : "" + <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#edit_channel" data-desc={channel.description} data-title={channel.display_name} data-channelid={channel.id}>Set Channel Description...</a></li> + <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#channel_notifications" data-title={channel.display_name} data-channelid={channel.id}>Notification Preferences</a></li> + { isAdmin && !ChannelStore.isDefault(channel) ? + <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#rename_channel" data-display={channel.display_name} data-name={channel.name} data-channelid={channel.id}>Rename Channel...</a></li> + : null } - { isAdmin && channelName != Constants.DEFAULT_CHANNEL ? - <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#delete_channel" data-title={this.state.channel.display_name} data-channelid={this.state.channel.id}>Delete Channel...</a></li> - : "" + { isAdmin && !ChannelStore.isDefault(channel) ? + <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#delete_channel" data-title={channel.display_name} data-channelid={channel.id}>Delete Channel...</a></li> + : null } - { channelName != Constants.DEFAULT_CHANNEL ? + { !ChannelStore.isDefault(channel) ? <li role="presentation"><a role="menuitem" href="#" onClick={this.handleLeave}>Leave Channel</a></li> - : "" + : null } </ul> </div> @@ -220,14 +200,13 @@ module.exports = React.createClass({ <a href="#"><strong className="heading">{channelTitle}</strong></a> } </th> - <th><ExtraMembers members={this.state.users} channelId={this.state.channel.id} /></th> - { searchForm } + <th><PopoverListMembers members={this.state.users} channelId={channel.id} /></th> + <th className="search-bar__container"><NavbarSearchBox /></th> <th> - <div className="dropdown" style={{"marginLeft":"5px", "marginRight":"10px"}}> + <div className="dropdown channel-header__links"> <a href="#" className="dropdown-toggle theme" type="button" id="channel_header_right_dropdown" data-toggle="dropdown" aria-expanded="true"> - <i className="fa fa-caret-down"></i> - </a> - <ul className="dropdown-menu" role="menu" aria-labelledby="channel_header_right_dropdown" style={{"left": "-150px"}}> + <span dangerouslySetInnerHTML={{__html: " <svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='4px' height='16px' viewBox='0 0 8 32' enable-background='new 0 0 8 32' xml:space='preserve'> <g> <circle cx='4' cy='4.062' r='4'/> <circle cx='4' cy='16' r='4'/> <circle cx='4' cy='28' r='4'/> </g> </svg>"}} /> </a> + <ul className="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="channel_header_right_dropdown"> <li role="presentation"><a role="menuitem" href="#" onClick={this.searchMentions}>Recent Mentions</a></li> </ul> </div> @@ -237,5 +216,3 @@ module.exports = React.createClass({ ); } }); - - diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx index 9e3feb25c..6ed0f0b34 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -43,7 +43,7 @@ module.exports = React.createClass({ client.createPost(post, ChannelStore.getCurrent(), function(data) { - this.setState({ messageText: '', submitting: false, post_error: null }); + this.setState({ messageText: '', submitting: false, post_error: null, server_error: null }); this.clearPreviews(); AsyncClient.getPosts(true, this.props.channelId); @@ -57,6 +57,7 @@ module.exports = React.createClass({ function(err) { var state = {}; state.server_error = err.message; + state.submitting = false; if (err.message === "Invalid RootId parameter") { if ($('#post_deleted').length > 0) $('#post_deleted').modal('show'); diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 0c23dcfac..d38a6798f 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -234,7 +234,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/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx index fefac12d7..11970bc2b 100644 --- a/web/react/components/delete_post_modal.jsx +++ b/web/react/components/delete_post_modal.jsx @@ -56,13 +56,13 @@ module.exports = React.createClass({ $(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) { var newState = {}; if(BrowserStore.getItem('edit_state_transfer')) { - newState = JSON.parse(BrowserStore.getItem('edit_state_transfer')); + newState = BrowserStore.getItem('edit_state_transfer'); BrowserStore.removeItem('edit_state_transfer'); } else { var button = e.relatedTarget; newState = { title: $(button).attr('data-title'), channel_id: $(button).attr('data-channelid'), post_id: $(button).attr('data-postid'), comments: $(button).attr('data-comments') }; } - self.setState(newState) + self.setState(newState); }); PostStore.addSelectedPostChangeListener(this._onChange); }, diff --git a/web/react/components/edit_channel_modal.jsx b/web/react/components/edit_channel_modal.jsx index 255654fd5..c0818959a 100644 --- a/web/react/components/edit_channel_modal.jsx +++ b/web/react/components/edit_channel_modal.jsx @@ -43,7 +43,7 @@ module.exports = React.createClass({ <h4 className="modal-title" ref="title">Edit {this.state.title} Description</h4> </div> <div className="modal-body"> - <textarea className="form-control" style={{resize: "none"}} rows="6" ref="channelDesc" maxLength="1024" value={this.state.description} onChange={this.handleUserInput}></textarea> + <textarea className="form-control no-resize" rows="6" ref="channelDesc" maxLength="1024" value={this.state.description} onChange={this.handleUserInput}></textarea> </div> <div className="modal-footer"> <button type="button" className="btn btn-default" data-dismiss="modal">Close</button> diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx index d741e189b..21b75bb6e 100644 --- a/web/react/components/edit_post_modal.jsx +++ b/web/react/components/edit_post_modal.jsx @@ -4,6 +4,7 @@ var Client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var Textbox = require('./textbox.jsx'); +var BrowserStore = require('../stores/browser_store.jsx'); module.exports = React.createClass({ handleEdit: function(e) { @@ -13,14 +14,14 @@ module.exports = React.createClass({ if (updatedPost.message.length === 0) { var tempState = this.state; delete tempState.editText; - BrowserStore.setItem('edit_state_transfer', JSON.stringify(tempState)); + BrowserStore.setItem('edit_state_transfer', tempState); $("#edit_post").modal('hide'); $("#delete_post").modal('show'); return; } - updatedPost.id = this.state.post_id - updatedPost.channel_id = this.state.channel_id + updatedPost.id = this.state.post_id; + updatedPost.channel_id = this.state.channel_id; Client.updatePost(updatedPost, function(data) { diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx index bbfdce63a..af5314e64 100644 --- a/web/react/components/get_link_modal.jsx +++ b/web/react/components/get_link_modal.jsx @@ -38,7 +38,7 @@ module.exports = React.createClass({ <div className="modal-body"> <p>{"The link below is used for open " + strings.TeamPlural + " or if you allowed your " + strings.Team + " members to sign up using their " + strings.Company + " email addresses."} </p> - <textarea className="form-control" style={{resize: "none"}} readOnly="true" value={this.state.value}></textarea> + <textarea className="form-control no-resize" readOnly="true" value={this.state.value}></textarea> </div> <div className="modal-footer"> <button type="button" className="btn btn-default" data-dismiss="modal">Close</button> diff --git a/web/react/components/loading_screen.jsx b/web/react/components/loading_screen.jsx new file mode 100644 index 000000000..5905e519b --- /dev/null +++ b/web/react/components/loading_screen.jsx @@ -0,0 +1,24 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +module.exports = React.createClass({ + displayName: "LoadingScreen", + propTypes: { + position: React.PropTypes.oneOf(['absolute', 'fixed', 'relative', 'static', 'inherit']) + }, + getDefaultProps: function() { + return { position: 'relative' }; + }, + render: function() { + return ( + <div className="loading-screen" style={{position: this.props.position}}> + <div className="loading__content"> + <h3>Loading</h3> + <div className="round round-1"></div> + <div className="round round-2"></div> + <div className="round round-3"></div> + </div> + </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_item.jsx b/web/react/components/member_list_item.jsx index 357fd49a8..a5279909b 100644 --- a/web/react/components/member_list_item.jsx +++ b/web/react/components/member_list_item.jsx @@ -23,6 +23,7 @@ module.exports = React.createClass({ var member = this.props.member; var isAdmin = this.props.isAdmin; var isMemberAdmin = member.roles.indexOf("admin") > -1; + var timestamp = UserStore.getCurrentUser().update_at; var invite; if (member.invited && this.props.handleInvite) { @@ -48,12 +49,12 @@ module.exports = React.createClass({ </div> ); } else { - invite = <div className="member-role text-capitalize" style={{marginRight: 15}}>{member.roles || 'Member'}</div>; + invite = <div className="member-role text-capitalize">{member.roles || 'Member'}<span className="caret hidden"></span></div>; } return ( <div className="row member-div"> - <img className="post-profile-img pull-left" src={"/api/v1/users/" + member.id + "/image"} height="36" width="36" /> + <img className="post-profile-img pull-left" src={"/api/v1/users/" + member.id + "/image?time=" + timestamp} height="36" width="36" /> <span className="member-name">{member.username}</span> <span className="member-email">{member.email}</span> { invite } diff --git a/web/react/components/member_list_team.jsx b/web/react/components/member_list_team.jsx index cfb473e5e..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() { @@ -59,9 +60,10 @@ var MemberListTeamItem = React.createClass({ return {}; }, render: function() { - var server_error = this.state.server_error ? <div style={{ clear: "both" }} className="has-error"><label className='has-error control-label'>{this.state.server_error}</label></div> : null; + var server_error = this.state.server_error ? <div className="has-error"><label className='has-error control-label'>{this.state.server_error}</label></div> : null; var user = this.props.user; - var currentRoles = "Member" + var currentRoles = "Member"; + var timestamp = UserStore.getCurrentUser().update_at; if (user.roles.length > 0) { currentRoles = user.roles.charAt(0).toUpperCase() + user.roles.slice(1); @@ -83,9 +85,9 @@ var MemberListTeamItem = React.createClass({ return ( <div className="row member-div"> - <img className="post-profile-img pull-left" src={"/api/v1/users/" + user.id + "/image"} height="36" width="36" /> - <span className="member-name">{user.full_name.trim() ? user.full_name : user.username}</span> - <span className="member-email">{user.full_name.trim() ? user.username : email}</span> + <img className="post-profile-img pull-left" src={"/api/v1/users/" + user.id + "/image?time=" + timestamp} height="36" width="36" /> + <span className="member-name">{utils.getDisplayName(user)}</span> + <span className="member-email">{email}</span> <div className="dropdown member-drop"> <a href="#" className="dropdown-toggle theme" type="button" id="channel_header_dropdown" data-toggle="dropdown" aria-expanded="true"> <span>{currentRoles} </span> diff --git a/web/react/components/mention.jsx b/web/react/components/mention.jsx index 3c33ddf49..114dc183f 100644 --- a/web/react/components/mention.jsx +++ b/web/react/components/mention.jsx @@ -1,19 +1,27 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. +var UserStore = require("../stores/user_store.jsx"); module.exports = React.createClass({ handleClick: function() { this.props.handleClick(this.props.username); }, + getInitialState: function() { + return null; + }, render: function() { + var self = this; var icon; - if (this.props.id != null) { - icon = <span><img className="mention-img" src={"/api/v1/users/" + this.props.id + "/image"}/></span>; + var timestamp = UserStore.getCurrentUser().update_at; + if (this.props.id === "allmention" || this.props.id === "channelmention") { + icon = <span><i className="mention-img fa fa-users fa-2x"></i></span>; + } else if (this.props.id != null) { + icon = <span><img className="mention-img" src={"/api/v1/users/" + this.props.id + "/image?time=" + timestamp}/></span>; } else { icon = <span><i className="mention-img fa fa-users fa-2x"></i></span>; } return ( - <div className="mentions-name" onClick={this.handleClick}> + <div className={"mentions-name " + this.props.isFocused} id={this.props.id + "_mentions"} onClick={this.handleClick} onMouseEnter={this.props.handleMouseEnter}> <div className="pull-left">{icon}</div> <div className="pull-left mention-align"><span>@{this.props.username}</span><span className="mention-fullname">{this.props.secondary_text}</span></div> </div> diff --git a/web/react/components/mention_list.jsx b/web/react/components/mention_list.jsx index ba2c53612..71a6083d2 100644 --- a/web/react/components/mention_list.jsx +++ b/web/react/components/mention_list.jsx @@ -7,19 +7,48 @@ var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var Mention = require('./mention.jsx'); var Constants = require('../utils/constants.jsx'); +var Utils = require('../utils/utils.jsx'); var ActionTypes = Constants.ActionTypes; +var MAX_HEIGHT_LIST = 292; +var MAX_ITEMS_IN_LIST = 25; +var ITEM_HEIGHT = 36; + module.exports = React.createClass({ + displayName: "MentionList", componentDidMount: function() { PostStore.addMentionDataChangeListener(this._onChange); - var self = this; - $('body').on('keypress.mentionlist', '#'+this.props.id, + + $('body').on('keydown.mentionlist', '#'+this.props.id, function(e) { - if (!self.isEmpty() && self.state.mentionText != '-1' && e.which === 13) { + if (!self.isEmpty() && self.state.mentionText != '-1' && (e.which === 13 || e.which === 9)) { e.stopPropagation(); e.preventDefault(); - self.addFirstMention(); + self.addCurrentMention(); + } + else if (!self.isEmpty() && self.state.mentionText != '-1' && (e.which === 38 || e.which === 40)) { + e.stopPropagation(); + e.preventDefault(); + + var tempSelectedMention = -1; + if (e.which === 38) { + if (self.getSelection(self.state.selectedMention - 1)) + self.setState({ selectedMention: self.state.selectedMention - 1, selectedUsername: self.refs['mention' + (self.state.selectedMention - 1)].props.username }); + else { + while (self.getSelection(++tempSelectedMention)) + ; //Need to find the top of the list + self.setState({ selectedMention: tempSelectedMention - 1, selectedUsername: self.refs['mention' + (tempSelectedMention - 1)].props.username }); + } + } + else if (e.which === 40) { + if (self.getSelection(self.state.selectedMention + 1)) + self.setState({ selectedMention: self.state.selectedMention + 1, selectedUsername: self.refs['mention' + (self.state.selectedMention + 1)].props.username }); + else + self.setState({ selectedMention: 0, selectedUsername: self.refs.mention0.props.username }); + } + + self.scrollToMention(e.which, tempSelectedMention); } } ); @@ -32,7 +61,28 @@ module.exports = React.createClass({ }, componentWillUnmount: function() { PostStore.removeMentionDataChangeListener(this._onChange); - $('body').off('keypress.mentionlist', '#'+this.props.id); + $('body').off('keydown.mentionlist', '#'+this.props.id); + }, + componentDidUpdate: function() { + if (this.state.mentionText != "-1") { + if (this.state.selectedUsername !== "" && (!this.getSelection(this.state.selectedMention) || this.state.selectedUsername !== this.refs['mention' + this.state.selectedMention].props.username)) { + var tempSelectedMention = -1; + var foundMatch = false; + while (tempSelectedMention < this.state.selectedMention && this.getSelection(++tempSelectedMention)) { + if (this.state.selectedUsername === this.refs['mention' + tempSelectedMention].props.username) { + this.setState({ selectedMention: tempSelectedMention }); + foundMatch = true; + break; + } + } + if (this.getSelection(0) && !foundMatch) { + this.setState({ selectedMention: 0, selectedUsername: this.refs.mention0.props.username }); + } + } + } + else if (this.state.selectedMention !== 0) { + this.setState({ selectedMention: 0, selectedUsername: "" }); + } }, _onChange: function(id, mentionText, excludeList) { if (id !== this.props.id) return; @@ -40,6 +90,7 @@ module.exports = React.createClass({ var newState = this.state; if (mentionText != null) newState.mentionText = mentionText; if (excludeList != null) newState.excludeUsers = excludeList; + this.setState(newState); }, handleClick: function(name) { @@ -51,6 +102,21 @@ module.exports = React.createClass({ this.setState({ mentionText: '-1' }); }, + handleMouseEnter: function(listId) { + this.setState({ selectedMention: listId, selectedUsername: this.refs['mention' + listId].props.username }); + }, + getSelection: function(listId) { + if (!this.refs['mention' + listId]) + return false; + else + return true; + }, + addCurrentMention: function() { + if (!this.getSelection(this.state.selectedMention)) + this.addFirstMention(); + else + this.refs['mention' + this.state.selectedMention].handleClick(); + }, addFirstMention: function() { if (!this.refs.mention0) return; this.refs.mention0.handleClick(); @@ -58,6 +124,23 @@ module.exports = React.createClass({ isEmpty: function() { return (!this.refs.mention0); }, + scrollToMention: function(keyPressed, ifLoopUp) { + var direction = keyPressed === 38 ? "up" : "down"; + var scrollAmount = 0; + + if (direction === "up" && ifLoopUp !== -1) + scrollAmount = $("#mentionsbox").height() * 100; //Makes sure that it scrolls all the way to the bottom + else if (direction === "down" && this.state.selectedMention === 0) + scrollAmount = 0; + else if (direction === "up") + scrollAmount = "-=" + ($('#'+this.refs['mention' + this.state.selectedMention].props.id +"_mentions").innerHeight() - 5); + else if (direction === "down") + scrollAmount = "+=" + ($('#'+this.refs['mention' + this.state.selectedMention].props.id +"_mentions").innerHeight() - 5); + + $("#mentionsbox").animate({ + scrollTop: scrollAmount + }, 75); + }, alreadyMentioned: function(username) { var excludeUsers = this.state.excludeUsers; for (var i = 0; i < excludeUsers.length; i++) { @@ -68,11 +151,12 @@ module.exports = React.createClass({ return false; }, getInitialState: function() { - return { excludeUsers: [], mentionText: "-1" }; + return { excludeUsers: [], mentionText: "-1", selectedMention: 0, selectedUsername: "" }; }, render: function() { + var self = this; var mentionText = this.state.mentionText; - if (mentionText === '-1') return (<div/>); + if (mentionText === '-1') return null; var profiles = UserStore.getActiveOnlyProfiles(); var users = []; @@ -82,14 +166,16 @@ module.exports = React.createClass({ var all = {}; all.username = "all"; - all.full_name = ""; + all.nickname = ""; all.secondary_text = "Notifies everyone in the team"; + all.id = "allmention"; users.push(all); var channel = {}; channel.username = "channel"; - channel.full_name = ""; + channel.nickname = ""; channel.secondary_text = "Notifies everyone in the channel"; + channel.id = "channelmention"; users.push(channel); users.sort(function(a,b) { @@ -100,44 +186,42 @@ module.exports = React.createClass({ var mentions = {}; var index = 0; - for (var i = 0; i < users.length; i++) { - if (Object.keys(mentions).length >= 25) break; + for (var i = 0; i < users.length && index < MAX_ITEMS_IN_LIST; i++) { if (this.alreadyMentioned(users[i].username)) continue; - var firstName = "", lastName = ""; - if (users[i].full_name.length > 0) { - var splitName = users[i].full_name.split(' '); - firstName = splitName[0].toLowerCase(); - lastName = splitName.length > 1 ? splitName[splitName.length-1].toLowerCase() : ""; - users[i].secondary_text = users[i].full_name; - } - - if (firstName.lastIndexOf(mentionText,0) === 0 - || lastName.lastIndexOf(mentionText,0) === 0 || users[i].username.lastIndexOf(mentionText,0) === 0) { - mentions[i+1] = ( + if ((users[i].first_name && users[i].first_name.lastIndexOf(mentionText,0) === 0) + || (users[i].last_name && users[i].last_name.lastIndexOf(mentionText,0) === 0) || users[i].username.lastIndexOf(mentionText,0) === 0) { + mentions[index] = ( <Mention ref={'mention' + index} username={users[i].username} - secondary_text={users[i].secondary_text} + secondary_text={Utils.getFullName(users[i])} id={users[i].id} + listId={index} + isFocused={this.state.selectedMention === index ? "mentions-focus" : ""} + handleMouseEnter={function(value) { return function() { self.handleMouseEnter(value); } }(index)} handleClick={this.handleClick} /> ); index++; } } + var numMentions = Object.keys(mentions).length; - if (numMentions < 1) return (<div/>); + if (numMentions < 1) return null; - var height = (numMentions*36) + 4; - var width = $('#'+this.props.id).parent().width(); - var bottom = $(window).height() - $('#'+this.props.id).offset().top; - var left = $('#'+this.props.id).offset().left; - var max_height = $('#'+this.props.id).offset().top - 10; + var $mention_tab = $('#'+this.props.id); + var maxHeight = Math.min(MAX_HEIGHT_LIST, $mention_tab.offset().top - 10); + var style = { + height: Math.min(maxHeight, (numMentions*ITEM_HEIGHT) + 4), + width: $mention_tab.parent().width(), + bottom: $(window).height() - $mention_tab.offset().top, + left: $mention_tab.offset().left + }; return ( - <div className="mentions--top" style={{height: height, width: width, bottom: bottom, left: left}}> - <div ref="mentionlist" className="mentions-box" style={{height: height, width: width}}> + <div className="mentions--top" style={style}> + <div ref="mentionlist" className="mentions-box" id="mentionsbox"> { mentions } </div> </div> diff --git a/web/react/components/more_channels.jsx b/web/react/components/more_channels.jsx index c3ddc76f3..007476f9b 100644 --- a/web/react/components/more_channels.jsx +++ b/web/react/components/more_channels.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 LoadingScreen = require('./loading_screen.jsx'); function getStateFromStores() { return { @@ -16,6 +17,8 @@ function getStateFromStores() { } module.exports = React.createClass({ + displayName: "MoreChannelsModal", + componentDidMount: function() { ChannelStore.addMoreChangeListener(this._onChange); $(this.refs.modal.getDOMNode()).on('shown.bs.modal', function (e) { @@ -90,7 +93,7 @@ module.exports = React.createClass({ <p className="more-channel-name">{channel.display_name}</p> <p className="more-channel-description">{channel.description}</p> </td> - <td className="td--action"><button onClick={outter.handleJoin.bind(outter, channel.id)} className="pull-right btn btn-primary">Join</button></td> + <td className="td--action"><button onClick={outter.handleJoin.bind(outter, channel.id)} className="btn btn-primary">Join</button></td> </tr> ) })} @@ -100,15 +103,7 @@ module.exports = React.createClass({ <p className="primary-message">No more channels to join</p> <p className="secondary-message">Click 'Create New Channel' to make a new one</p> </div>) - : <div ref="loadingscreen" className="loading-screen loading-screen--channel"> - <div className="loading__content"> - <h3>Loading</h3> - <div id="round_1" className="round"></div> - <div id="round_2" className="round"></div> - <div id="round_3" className="round"></div> - </div> - </div> - } + : <LoadingScreen /> } { server_error } </div> <div className="modal-footer"> 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 35f7d9044..34c65c34f 100644 --- a/web/react/components/navbar.jsx +++ b/web/react/components/navbar.jsx @@ -2,21 +2,21 @@ // See License.txt for license information. -var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var utils = require('../utils/utils.jsx'); var client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); -var Sidebar = require('./sidebar.jsx'); var UserStore = require('../stores/user_store.jsx'); -var SocketStore = require('../stores/socket_store.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); -var Constants = require('../utils/constants.jsx'); +var TeamStore = require('../stores/team_store.jsx'); + var UserProfile = require('./user_profile.jsx'); var MessageWrapper = require('./message_wrapper.jsx'); + +var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; +var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); function getCountsStateFromStores() { - var count = 0; var channels = ChannelStore.getAll(); var members = ChannelStore.getAllMembers(); @@ -34,7 +34,7 @@ function getCountsStateFromStores() { } }); - return { count: count } + return { count: count }; } var NotifyCounts = React.createClass({ @@ -54,99 +54,11 @@ var NotifyCounts = React.createClass({ return getCountsStateFromStores(); }, render: function() { - if (this.state.count == 0) { - return (<span></span>); - } - else { - return (<span className="badge badge-notify">{ this.state.count }</span>); - } - } -}); - -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'; - } - - }.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"; + if (this.state.count) { + return <span className="badge badge-notify">{ this.state.count }</span>; + } else { + return null; } - - 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> - ); } }); @@ -159,13 +71,14 @@ function getStateFromStores() { } module.exports = React.createClass({ + displayName: 'Navbar', + componentDidMount: function() { ChannelStore.addChangeListener(this._onChange); ChannelStore.addExtraInfoChangeListener(this._onChange); - var self = this; - $('.inner__wrap').click(self.hideSidebars); + $('.inner__wrap').click(this.hideSidebars); - $('body').on('click.infopopover', function(e){ + $('body').on('click.infopopover', function(e) { if ($(e.target).attr('data-toggle') !== 'popover' && $(e.target).parents('.popover.in').length === 0) { $('.info-popover').popover('hide'); @@ -181,13 +94,13 @@ module.exports = React.createClass({ }, handleLeave: function(e) { client.leaveChannel(this.state.channel.id, - function(data) { + 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"); - }.bind(this) + } ); }, hideSidebars: function(e) { @@ -204,7 +117,7 @@ module.exports = React.createClass({ }); if (e.target.className != 'navbar-toggle' && e.target.className != 'icon-bar') { - $('.inner__wrap').removeClass('move--right').removeClass('move--left').removeClass('move--left-small'); + $('.inner__wrap').removeClass('move--right move--left move--left-small'); $('.sidebar--left').removeClass('move--right'); $('.sidebar--right').removeClass('move--left'); $('.sidebar--menu').removeClass('move--left'); @@ -229,24 +142,22 @@ module.exports = React.createClass({ render: function() { var currentId = UserStore.getCurrentId(); - var channelName = ""; var popoverContent = ""; - var channelTitle = this.props.teamName; + var channelTitle = this.props.teamDisplayName; var isAdmin = false; var isDirect = false; var description = "" + var channel = this.state.channel; - if (this.state.channel) { - var channel = this.state.channel; - description = utils.textToJsx(this.state.channel.description, {"singleline": true, "noMentionHighlight": true}); - popoverContent = React.renderToString(<MessageWrapper message={this.state.channel.description}/>); - channelName = this.state.channel.name; + if (channel) { + description = utils.textToJsx(channel.description, {"singleline": true, "noMentionHighlight": true}); + popoverContent = React.renderToString(<MessageWrapper message={channel.description}/>); isAdmin = this.state.member.roles.indexOf("admin") > -1; if (channel.type === 'O') { - channelTitle = this.state.channel.display_name; + channelTitle = channel.display_name; } else if (channel.type === 'P') { - channelTitle = this.state.channel.display_name; + channelTitle = channel.display_name; } else if (channel.type === 'D') { isDirect = true; if (this.state.users.length > 1) { @@ -258,12 +169,11 @@ module.exports = React.createClass({ } } - if(this.state.channel.description.length == 0){ - popoverContent = React.renderToString(<div>No channel description yet. <br /><a href='#' data-toggle='modal' data-desc={this.state.channel.description} data-title={this.state.channel.display_name} data-channelid={this.state.channel.id} data-target='#edit_channel'>Click here</a> to add one.</div>); + if (channel.description.length == 0) { + popoverContent = React.renderToString(<div>No channel description yet. <br /><a href='#' data-toggle='modal' data-desc={channel.description} data-title={channel.display_name} data-channelid={channel.id} data-target='#edit_channel'>Click here</a> to add one.</div>); } } - var loginForm = currentId == null ? <NavbarLoginForm /> : null; var navbar_collapse_button = currentId != null ? null : <button type="button" className="navbar-toggle" data-toggle="collapse" data-target="#navbar-collapse-1"> <span className="sr-only">Toggle sidebar</span> @@ -292,60 +202,55 @@ module.exports = React.createClass({ { navbar_collapse_button } { sidebar_collapse_button } { right_sidebar_collapse_button } - { !isDirect && this.state.channel ? - <div className="navbar-brand"> - <div className="dropdown"> - <div data-toggle="popover" data-content={popoverContent} className="description info-popover"></div> - <a href="#" className="dropdown-toggle theme" type="button" id="channel_header_dropdown" data-toggle="dropdown" aria-expanded="true"> - <strong className="heading">{channelTitle} </strong> - <span className="glyphicon glyphicon-chevron-down header-dropdown__icon"></span> - </a> - <ul className="dropdown-menu" role="menu" aria-labelledby="channel_header_dropdown"> - <li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_invite" href="#">Add Members</a></li> - { isAdmin ? - <li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_members" href="#">Manage Members</a></li> - : "" - } - <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#edit_channel" data-desc={this.state.channel.description} data-title={this.state.channel.display_name} data-channelid={this.state.channel.id}>Set Channel Description...</a></li> - <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#channel_notifications" data-title={this.state.channel.display_name} data-channelid={this.state.channel.id}>Notification Preferences</a></li> - { isAdmin && channelName != Constants.DEFAULT_CHANNEL ? - <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#rename_channel" data-display={this.state.channel.display_name} data-name={this.state.channel.name} data-channelid={this.state.channel.id}>Rename Channel...</a></li> - : "" - } - { isAdmin && channelName != Constants.DEFAULT_CHANNEL ? - <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#delete_channel" data-title={this.state.channel.display_name} data-channelid={this.state.channel.id}>Delete Channel...</a></li> - : "" - } - { channelName != Constants.DEFAULT_CHANNEL ? - <li role="presentation"><a role="menuitem" href="#" onClick={this.handleLeave}>Leave Channel</a></li> - : "" - } - </ul> + { !isDirect && channel ? + <div className="navbar-brand"> + <div className="dropdown"> + <div data-toggle="popover" data-content={popoverContent} className="description info-popover"></div> + <a href="#" className="dropdown-toggle theme" type="button" id="channel_header_dropdown" data-toggle="dropdown" aria-expanded="true"> + <span className="heading">{channelTitle} </span> + <span className="glyphicon glyphicon-chevron-down header-dropdown__icon"></span> + </a> + <ul className="dropdown-menu" role="menu" aria-labelledby="channel_header_dropdown"> + { !ChannelStore.isDefault(channel) ? + <li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_invite" href="#">Add Members</a></li> + : null + } + { isAdmin && !ChannelStore.isDefault(channel) ? + <li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_members" href="#">Manage Members</a></li> + : null + } + <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#edit_channel" data-desc={channel.description} data-title={channel.display_name} data-channelid={channel.id}>Set Channel Description...</a></li> + <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#channel_notifications" data-title={channel.display_name} data-channelid={channel.id}>Notification Preferences</a></li> + { isAdmin && !ChannelStore.isDefault(channel) ? + <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#rename_channel" data-display={channel.display_name} data-name={channel.name} data-channelid={channel.id}>Rename Channel...</a></li> + : null + } + { isAdmin && !ChannelStore.isDefault(channel) ? + <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#delete_channel" data-title={channel.display_name} data-channelid={channel.id}>Delete Channel...</a></li> + : null + } + { !ChannelStore.isDefault(channel) ? + <li role="presentation"><a role="menuitem" href="#" onClick={this.handleLeave}>Leave Channel</a></li> + : null + } + </ul> + </div> </div> - </div> - : "" } - { isDirect && this.state.channel ? + : null + } + { isDirect && channel ? <div className="navbar-brand"> - <strong> - <a href="#"><strong className="heading">{channelTitle}</strong></a> - </strong> + <a href="#" className="heading">{ channelTitle }</a> </div> - : "" } - { !this.state.channel ? + : null } + { !channel ? <div className="navbar-brand"> - <strong> - <a href="/"><strong className="heading">{ channelTitle }</strong></a> - </strong> + <a href="/" className="heading">{ channelTitle }</a> </div> : "" } </div> - <div className="collapse navbar-collapse" id="navbar-collapse-1"> - { loginForm } - </div> </div> </nav> ); -} + } }); - - diff --git a/web/react/components/new_channel.jsx b/web/react/components/new_channel.jsx index 160241c1c..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) { @@ -122,7 +124,7 @@ module.exports = React.createClass({ </div> <div className="form-group"> <label className='control-label'>Description</label> - <textarea className="form-control" style={{resize: "none"}} ref="channel_desc" rows="3" placeholder="Description" maxLength="1024"></textarea> + <textarea className="form-control no-resize" ref="channel_desc" rows="3" placeholder="Description" maxLength="1024"></textarea> </div> { server_error } </form> 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 04b5ba082..e72a2d001 100644 --- a/web/react/components/post.jsx +++ b/web/react/components/post.jsx @@ -10,6 +10,7 @@ var UserStore = require('../stores/user_store.jsx'); var ActionTypes = Constants.ActionTypes; module.exports = React.createClass({ + displayName: "Post", componentDidMount: function() { $('.modal').on('show.bs.modal', function () { $('.modal-body').css('overflow-y', 'auto'); @@ -19,7 +20,7 @@ module.exports = React.createClass({ handleCommentClick: function(e) { e.preventDefault(); - data = {}; + var data = {}; data.order = [this.props.post.id]; data.posts = this.props.posts; @@ -33,6 +34,10 @@ module.exports = React.createClass({ results: null }); }, + forceUpdateInfo: function() { + this.refs.info.forceUpdate(); + this.refs.header.forceUpdate(); + }, getInitialState: function() { return { }; }, @@ -48,7 +53,6 @@ module.exports = React.createClass({ var commentCount = 0; var commentRootId = parentPost ? post.root_id : post.id; - var rootUser = ""; for (var postId in posts) { if (posts[postId].root_id == commentRootId) { commentCount += 1; @@ -57,12 +61,7 @@ module.exports = React.createClass({ var error = this.state.error ? <div className='form-group has-error'><label className='control-label'>{ this.state.error }</label></div> : null; - if (this.props.sameRoot){ - rootUser = "same--root"; - } - else { - rootUser = "other--root"; - } + var rootUser = this.props.sameRoot ? "same--root" : "other--root"; var postType = ""; if (type != "Post"){ @@ -74,18 +73,20 @@ module.exports = React.createClass({ currentUserCss = "current--user"; } + var timestamp = UserStore.getCurrentUser().update_at; + return ( <div> <div id={post.id} className={"post " + this.props.sameUser + " " + rootUser + " " + postType + " " + currentUserCss}> { !this.props.hideProfilePic ? <div className="post-profile-img__container"> - <img className="post-profile-img" src={"/api/v1/users/" + post.user_id + "/image"} height="36" width="36" /> + <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"> - <PostHeader post={post} sameRoot={this.props.sameRoot} commentCount={commentCount} handleCommentClick={this.handleCommentClick} isLastComment={this.props.isLastComment} /> + <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 post={post} sameRoot={this.props.sameRoot} commentCount={commentCount} handleCommentClick={this.handleCommentClick} allowReply="true" /> + <PostInfo ref="info" post={post} sameRoot={this.props.sameRoot} commentCount={commentCount} handleCommentClick={this.handleCommentClick} allowReply="true" /> </div> </div> </div> diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index 7d5ef4d33..d9678df30 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -71,11 +71,22 @@ module.exports = React.createClass({ name = <a className="theme" onClick={function(){ utils.searchForTerm(profile.username); }}>{profile.username}</a>; } - var message = parentPost.message; + var message = "" + if(parentPost.message) { + message = utils.replaceHtmlEntities(parentPost.message) + } else if (parentPost.filenames.length) { + message = parentPost.filenames[0].split('/').pop(); + + if (parentPost.filenames.length === 2) { + message += " plus 1 other file"; + } else if (parentPost.filenames.length > 2) { + message += " plus " + (parentPost.filenames.length - 1) + " other files"; + } + } comment = ( <p className="post-link"> - <span>Commented on {name}{apostrophe} message: <a className="theme" onClick={this.props.handleCommentClick}>{utils.replaceHtmlEntities(message)}</a></span> + <span>Commented on {name}{apostrophe} message: <a className="theme" onClick={this.props.handleCommentClick}>{message}</a></span> </p> ); @@ -120,7 +131,7 @@ module.exports = React.createClass({ return ( <div className="post-body"> { comment } - <p key={post.Id+"_message"} className={postClass}><span>{inner}</span></p> + <p key={post.id+"_message"} className={postClass}><span>{inner}</span></p> { filenames && filenames.length > 0 ? <div className="post-image__columns"> { postFiles } diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx index cf01747f0..d6422fe3a 100644 --- a/web/react/components/post_info.jsx +++ b/web/react/components/post_info.jsx @@ -11,6 +11,7 @@ module.exports = React.createClass({ render: function() { var post = this.props.post; var isOwner = UserStore.getCurrentId() == post.user_id; + var isAdmin = UserStore.getCurrentUser().roles.indexOf("admin") > -1 var type = "Post" if (post.root_id.length > 0) { @@ -30,13 +31,11 @@ module.exports = React.createClass({ <div className="dropdown"> { isOwner || (this.props.allowReply === "true" && type != "Comment") ? <div> - <a href="#" className="dropdown-toggle theme" type="button" data-toggle="dropdown" aria-expanded="false"> - [...] - </a> + <a href="#" className="dropdown-toggle theme" type="button" data-toggle="dropdown" aria-expanded="false" /> <ul className="dropdown-menu" role="menu"> { isOwner ? <li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#edit_post" data-title={type} data-message={post.message} data-postid={post.id} data-channelid={post.channel_id} data-comments={type === "Post" ? this.props.commentCount : 0}>Edit</a></li> : "" } - { isOwner ? <li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#delete_post" data-title={type} data-postid={post.id} data-channelid={post.channel_id} data-comments={type === "Post" ? this.props.commentCount : 0}>Delete</a></li> + { isOwner || isAdmin ? <li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#delete_post" data-title={type} data-postid={post.id} data-channelid={post.channel_id} data-comments={type === "Post" ? this.props.commentCount : 0}>Delete</a></li> : "" } { this.props.allowReply === "true" ? <li role="presentation"><a className="reply-link theme" href="#" onClick={this.props.handleCommentClick}>Reply</a></li> : "" } diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index d6dc9ce30..c058455ba 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -6,8 +6,8 @@ var ChannelStore = require('../stores/channel_store.jsx'); var UserStore = require('../stores/user_store.jsx'); var UserProfile = require( './user_profile.jsx' ); var AsyncClient = require('../utils/async_client.jsx'); -var CreatePost = require('./create_post.jsx'); var Post = require('./post.jsx'); +var LoadingScreen = require('./loading_screen.jsx'); var SocketStore = require('../stores/socket_store.jsx'); var utils = require('../utils/utils.jsx'); var Client = require('../utils/client.jsx'); @@ -26,37 +26,8 @@ function getStateFromStores() { }; } -function changeColor(col, amt) { - - var usePound = false; - - if (col[0] == "#") { - col = col.slice(1); - usePound = true; - } - - var num = parseInt(col,16); - - var r = (num >> 16) + amt; - - if (r > 255) r = 255; - else if (r < 0) r = 0; - - var b = ((num >> 8) & 0x00FF) + amt; - - if (b > 255) b = 255; - else if (b < 0) b = 0; - - var g = (num & 0x0000FF) + amt; - - if (g > 255) g = 255; - else if (g < 0) g = 0; - - return (usePound?"#":"") + String("000000" + (g | (b << 8) | (r << 16)).toString(16)).slice(-6); - -} - module.exports = React.createClass({ + displayName: "PostList", scrollPosition: 0, preventScrollTrigger: false, gotMorePosts: false, @@ -69,7 +40,7 @@ module.exports = React.createClass({ utils.changeCss('a.theme', 'color:'+user.props.theme+'; fill:'+user.props.theme+'!important;'); utils.changeCss('div.theme', 'background-color:'+user.props.theme+';'); utils.changeCss('.btn.btn-primary', 'background: ' + user.props.theme+';'); - utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + changeColor(user.props.theme, -10) +';'); + utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, -10) +';'); utils.changeCss('.modal .modal-header', 'background: ' + user.props.theme+';'); utils.changeCss('.mention', 'background: ' + user.props.theme+';'); utils.changeCss('.mention-link', 'color: ' + user.props.theme+';'); @@ -78,6 +49,7 @@ module.exports = React.createClass({ PostStore.addChangeListener(this._onChange); ChannelStore.addChangeListener(this._onChange); + UserStore.addStatusesChangeListener(this._onTimeChange); SocketStore.addChangeListener(this._onSocketChange); $(".post-list-holder-by-time").perfectScrollbar(); @@ -157,28 +129,25 @@ module.exports = React.createClass({ componentWillUnmount: function() { PostStore.removeChangeListener(this._onChange); ChannelStore.removeChangeListener(this._onChange); + UserStore.removeStatusesChangeListener(this._onTimeChange); SocketStore.removeChangeListener(this._onSocketChange); $('body').off('click.userpopover'); }, resize: function() { + var post_holder = $(".post-list-holder-by-time")[0]; + this.preventScrollTrigger = true; if (this.gotMorePosts) { this.gotMorePosts = false; - var post_holder = $(".post-list-holder-by-time")[0]; - this.preventScrollTrigger = true; $(post_holder).scrollTop($(post_holder).scrollTop() + (post_holder.scrollHeight-this.oldScrollHeight) ); - $(post_holder).perfectScrollbar('update'); } else { - var post_holder = $(".post-list-holder-by-time")[0]; - this.preventScrollTrigger = true; if ($("#new_message")[0] && !this.scrolledToNew) { $(post_holder).scrollTop($(post_holder).scrollTop() + $("#new_message").offset().top - 63); - $(post_holder).perfectScrollbar('update'); this.scrolledToNew = true; } else { $(post_holder).scrollTop(post_holder.scrollHeight); - $(post_holder).perfectScrollbar('update'); } } + $(post_holder).perfectScrollbar('update'); }, _onChange: function() { var newState = getStateFromStores(); @@ -239,12 +208,8 @@ module.exports = React.createClass({ var index = post_list.order.indexOf(msg.props.post_id); if (index > -1) post_list.order.splice(index, 1); - var scrollSave = $(".post-list-holder-by-time").scrollTop(); - this.setState({ post_list: post_list }); - $(".post-list-holder-by-time").scrollTop(scrollSave) - PostStore.storePosts(msg.channel_id, post_list); } else { AsyncClient.getPosts(true, msg.channel_id); @@ -253,10 +218,16 @@ module.exports = React.createClass({ if (activeRootPostId === msg.props.post_id && UserStore.getCurrentId() != msg.user_id) { $('#post_deleted').modal('show'); } - } else if(msg.action == "new_user") { + } else if (msg.action == "new_user") { AsyncClient.getProfiles(); } }, + _onTimeChange: function() { + for (var id in this.state.post_list.posts) { + if (!this.refs[id]) continue; + this.refs[id].forceUpdateInfo(); + } + }, getMorePosts: function(e) { e.preventDefault(); @@ -297,7 +268,7 @@ module.exports = React.createClass({ }, function(err) { $(self.refs.loadmore.getDOMNode()).text("Load more messages"); - dispatchError(err, "getPosts"); + AsyncClient.dispatchError(err, "getPosts"); } ); }, @@ -338,16 +309,19 @@ module.exports = React.createClass({ var teammate = utils.getDirectTeammate(channel.id) if (teammate) { - var teammate_name = teammate.full_name.length > 0 ? teammate.full_name : teammate.username; + var teammate_name = teammate.nickname.length > 0 ? teammate.nickname : teammate.username; more_messages = ( <div className="channel-intro"> <div className="post-profile-img__container channel-intro-img"> - <img className="post-profile-img" src={"/api/v1/users/" + teammate.id + "/image"} height="50" width="50" /> + <img className="post-profile-img" src={"/api/v1/users/" + teammate.id + "/image?time=" + teammate.update_at} height="50" width="50" /> </div> <div className="channel-intro-profile"> <strong><UserProfile userId={teammate.id} /></strong> </div> - <p className="channel-intro-text">{"This is the start of your private message history with " + teammate_name + "." }<br/>{"Private messages and files shared here are not shown to people outside this area."}</p> + <p className="channel-intro-text"> + {"This is the start of your private message history with " + teammate_name + "." }<br/> + {"Private messages and files shared here are not shown to people outside this area."} + </p> </div> ); } else { @@ -370,7 +344,7 @@ module.exports = React.createClass({ } } - if (channel.name === Constants.DEFAULT_CHANNEL) { + if (ChannelStore.isDefault(channel)) { more_messages = ( <div className="channel-intro"> <h4 className="channel-intro-title">Welcome</h4> @@ -410,7 +384,7 @@ module.exports = React.createClass({ { 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" style={userStyle} data-toggle="modal" data-target="#channel_invite"><i className="fa fa-user-plus"></i>Invite others to this {ui_type}</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> </div> ); @@ -420,37 +394,35 @@ module.exports = React.createClass({ var postCtls = []; - if (posts != undefined) { + if (posts) { var previousPostDay = posts[order[order.length-1]] ? utils.getDateForUnixTicks(posts[order[order.length-1]].create_at): new Date(); - var currentPostDay = new Date(); + var currentPostDay; for (var i = order.length-1; i >= 0; i--) { var post = posts[order[i]]; - var parentPost; + var parentPost = post.parent_id ? posts[post.parent_id] : null; - if (post.parent_id) { - parentPost = posts[post.parent_id]; - } else { - parentPost = null; - } + var sameUser = ''; + var sameRoot = false; + var hideProfilePic = false; + var prevPost = (i < order.length - 1) ? posts[order[i + 1]] : null; - var sameUser = i < order.length-1 && posts[order[i+1]].user_id === post.user_id && post.create_at - posts[order[i+1]].create_at <= 1000*60*5 ? "same--user" : ""; - var sameRoot = i < order.length-1 && post.root_id != "" && (posts[order[i+1]].id === post.root_id || posts[order[i+1]].root_id === post.root_id) ? true : false; + if (prevPost) { + sameUser = (prevPost.user_id === post.user_id) && (post.create_at - prevPost.create_at <= 1000*60*5) ? "same--user" : ""; + sameRoot = utils.isComment(post) && (prevPost.id === post.root_id || prevPost.root_id === post.root_id); - // we only hide the profile pic if the previous post is not a comment, the current post is not a comment, and the previous post was made by the same user as the current post - var hideProfilePic = i < order.length-1 && posts[order[i+1]].user_id === post.user_id && posts[order[i+1]].root_id === '' && post.root_id === ''; + // we only hide the profile pic if the previous post is not a comment, the current post is not a comment, and the previous post was made by the same user as the current post + hideProfilePic = (prevPost.user_id === post.user_id) && !utils.isComment(prevPost) && !utils.isComment(post); + } // check if it's the last comment in a consecutive string of comments on the same post - var isLastComment = false; - if (utils.isComment(post)) { - // it is the last comment if it is last post in the channel or the next post has a different root post - isLastComment = (i === 0 || posts[order[i-1]].root_id != post.root_id); - } + // 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 sameUser={sameUser} sameRoot={sameRoot} post={post} parentPost={parentPost} key={post.id} posts={posts} hideProfilePic={hideProfilePic} isLastComment={isLastComment} />; + var postCtl = <Post ref={post.id} sameUser={sameUser} sameRoot={sameRoot} post={post} parentPost={parentPost} key={post.id} posts={posts} hideProfilePic={hideProfilePic} isLastComment={isLastComment} />; currentPostDay = utils.getDateForUnixTicks(post.create_at); - if(currentPostDay.getDate() !== previousPostDay.getDate() || currentPostDay.getMonth() !== previousPostDay.getMonth() || currentPostDay.getFullYear() !== previousPostDay.getFullYear()) { + if (currentPostDay.toDateString() != previousPostDay.toDateString()) { postCtls.push( <div className="date-separator"> <hr className="separator__hr" /> @@ -469,20 +441,10 @@ module.exports = React.createClass({ ); } postCtls.push(postCtl); - previousPostDay = utils.getDateForUnixTicks(post.create_at); + previousPostDay = currentPostDay; } - } - else { - postCtls.push( - <div ref="loadingscreen" className="loading-screen"> - <div className="loading__content"> - <h3>Loading</h3> - <div id="round_1" className="round"></div> - <div id="round_2" className="round"></div> - <div id="round_3" className="round"></div> - </div> - </div> - ); + } else { + postCtls.push(<LoadingScreen position="absolute" />); } return ( @@ -497,5 +459,3 @@ module.exports = React.createClass({ ); } }); - - diff --git a/web/react/components/post_right.jsx b/web/react/components/post_right.jsx index 115ee87d4..581a1abe9 100644 --- a/web/react/components/post_right.jsx +++ b/web/react/components/post_right.jsx @@ -43,7 +43,7 @@ RhsHeaderPost = React.createClass({ }); }, render: function() { - var back = this.props.fromSearch ? <a href="#" onClick={this.handleBack} style={{color:"black"}}>{"< "}</a> : ""; + var back = this.props.fromSearch ? <a href="#" onClick={this.handleBack} className="sidebar--right__back"><i className="fa fa-chevron-left"></i></a> : ""; return ( <div className="sidebar--right__header"> @@ -67,6 +67,8 @@ RootPost = React.createClass({ var message = utils.textToJsx(this.props.post.message); 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) { @@ -78,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 = []; @@ -117,8 +123,9 @@ 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"} height="36" width="36" /> + <img className="post-profile-img" src={"/api/v1/users/" + this.props.post.user_id + "/image?time=" + timestamp} height="36" width="36" /> </div> <div className="post__content"> <ul className="post-header"> @@ -128,9 +135,7 @@ RootPost = React.createClass({ <div className="dropdown"> { isOwner ? <div> - <a href="#" className="dropdown-toggle theme" type="button" data-toggle="dropdown" aria-expanded="false"> - [...] - </a> + <a href="#" className="dropdown-toggle theme" type="button" data-toggle="dropdown" aria-expanded="false" /> <ul className="dropdown-menu" role="menu"> <li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#edit_post" data-title={type} data-message={this.props.post.message} data-postid={this.props.post.id} data-channelid={this.props.post.channel_id}>Edit</a></li> <li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#delete_post" data-title={type} data-postid={this.props.post.id} data-channelid={this.props.post.channel_id} data-comments={this.props.commentCount}>Delete</a></li> @@ -227,11 +232,12 @@ CommentPost = React.createClass({ } var message = utils.textToJsx(this.props.post.message); + var timestamp = UserStore.getCurrentUser().update_at; return ( <div className={commentClass + " " + currentUserCss}> <div className="post-profile-img__container"> - <img className="post-profile-img" src={"/api/v1/users/" + this.props.post.user_id + "/image"} height="36" width="36" /> + <img className="post-profile-img" src={"/api/v1/users/" + this.props.post.user_id + "/image?time=" + timestamp} height="36" width="36" /> </div> <div className="post__content"> <ul className="post-header"> @@ -240,9 +246,7 @@ CommentPost = React.createClass({ <li className="post-header-col post-header__reply"> { isOwner ? <div className="dropdown" onClick={function(e){$('.post-list-holder-by-time').scrollTop($(".post-list-holder-by-time").scrollTop() + 50);}}> - <a href="#" className="dropdown-toggle theme" type="button" data-toggle="dropdown" aria-expanded="false"> - [...] - </a> + <a href="#" className="dropdown-toggle theme" type="button" data-toggle="dropdown" aria-expanded="false" /> <ul className="dropdown-menu" role="menu"> <li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#edit_post" data-title={type} data-message={this.props.post.message} data-postid={this.props.post.id} data-channelid={this.props.post.channel_id}>Edit</a></li> <li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#delete_post" data-title={type} data-postid={this.props.post.id} data-channelid={this.props.post.channel_id} data-comments={0}>Delete</a></li> @@ -282,6 +286,7 @@ module.exports = React.createClass({ componentDidMount: function() { PostStore.addSelectedPostChangeListener(this._onChange); PostStore.addChangeListener(this._onChangeAll); + UserStore.addStatusesChangeListener(this._onTimeChange); this.resize(); var self = this; $(window).resize(function(){ @@ -294,6 +299,7 @@ module.exports = React.createClass({ componentWillUnmount: function() { PostStore.removeSelectedPostChangeListener(this._onChange); PostStore.removeChangeListener(this._onChangeAll); + UserStore.removeStatusesChangeListener(this._onTimeChange); }, _onChange: function() { if (this.isMounted()) { @@ -304,7 +310,6 @@ module.exports = React.createClass({ } }, _onChangeAll: function() { - if (this.isMounted()) { // if something was changed in the channel like adding a @@ -333,6 +338,12 @@ module.exports = React.createClass({ this.setState(getStateFromStores()); } }, + _onTimeChange: function() { + for (var id in this.state.post_list.posts) { + if (!this.refs[id]) continue; + this.refs[id].forceUpdate(); + } + }, getInitialState: function() { return getStateFromStores(); }, @@ -392,7 +403,7 @@ module.exports = React.createClass({ <RootPost post={root_post} commentCount={posts_array.length}/> <div className="post-right-comments-container"> { posts_array.map(function(cpost) { - return <CommentPost key={cpost.id} post={cpost} selected={ (cpost.id == selected_post.id) } /> + return <CommentPost ref={cpost.id} key={cpost.id} post={cpost} selected={ (cpost.id == selected_post.id) } /> })} </div> <div className="post-create__container"> 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_bar.jsx b/web/react/components/search_bar.jsx index cddb738f9..f21f0cd58 100644 --- a/web/react/components/search_bar.jsx +++ b/web/react/components/search_bar.jsx @@ -3,6 +3,7 @@ var client = require('../utils/client.jsx'); +var AsyncClient = require('../utils/async_client.jsx'); var PostStore = require('../stores/post_store.jsx'); var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var utils = require('../utils/utils.jsx'); @@ -10,14 +11,14 @@ var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; function getSearchTermStateFromStores() { - term = PostStore.getSearchTerm(); - if (!term) term = ""; + var term = PostStore.getSearchTerm() || ''; return { search_term: term }; } module.exports = React.createClass({ + displayName: 'SearchBar', componentDidMount: function() { PostStore.addSearchTermChangeListener(this._onChange); }, @@ -58,14 +59,14 @@ module.exports = React.createClass({ e.target.select(); }, performSearch: function(terms, isMentionSearch) { - if (terms.length > 0) { - $("#search-spinner").removeClass("hidden"); + if (terms.length) { + this.setState({isSearching: true}); client.search( terms, function(data) { - $("#search-spinner").addClass("hidden"); - if(utils.isMobile()) { - $('#search')[0].value = ""; + this.setState({isSearching: false}); + if (utils.isMobile()) { + React.findDOMNode(this.refs.search).value = ''; } AppDispatcher.handleServerAction({ @@ -73,18 +74,17 @@ module.exports = React.createClass({ results: data, is_mention_search: isMentionSearch }); - }, + }.bind(this), function(err) { - $("#search-spinner").addClass("hidden"); - dispatchError(err, "search"); - } + this.setState({isSearching: false}); + AsyncClient.dispatchError(err, "search"); + }.bind(this) ); } }, handleSubmit: function(e) { e.preventDefault(); - terms = this.state.search_term.trim(); - this.performSearch(terms); + this.performSearch(this.state.search_term.trim()); }, getInitialState: function() { return getSearchTermStateFromStores(); @@ -92,11 +92,18 @@ module.exports = React.createClass({ render: function() { return ( <div> - <div className="sidebar__collapse" onClick={this.handleClose}></div> + <div className="sidebar__collapse" onClick={this.handleClose}>Cancel</div> <span className="glyphicon glyphicon-search sidebar__search-icon"></span> <form role="form" className="search__form relative-div" onSubmit={this.handleSubmit}> - <input type="text" className="form-control search-bar-box" ref="search" id="search" placeholder="Search" value={this.state.search_term} onFocus={this.handleUserFocus} onChange={this.handleUserInput} /> - <span id="search-spinner" className="glyphicon glyphicon-refresh glyphicon-refresh-animate hidden"></span> + <input + type="text" + ref="search" + className="form-control search-bar-box" + placeholder="Search" + value={this.state.search_term} + onFocus={this.handleUserFocus} + onChange={this.handleUserInput} /> + {this.state.isSearching ? <span className={"glyphicon glyphicon-refresh glyphicon-refresh-animate"}></span> : null} </form> </div> ); diff --git a/web/react/components/search_results.jsx b/web/react/components/search_results.jsx index 003a38b7e..643ad112b 100644 --- a/web/react/components/search_results.jsx +++ b/web/react/components/search_results.jsx @@ -8,12 +8,13 @@ var UserStore = require('../stores/user_store.jsx'); var UserProfile = require( './user_profile.jsx' ); var SearchBox =require('./search_bar.jsx'); var utils = require('../utils/utils.jsx'); -var client =require('../utils/client.jsx'); +var client = require('../utils/client.jsx'); +var AsyncClient = require('../utils/async_client.jsx'); var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; -RhsHeaderSearch = React.createClass({ +var RhsHeaderSearch = React.createClass({ handleClose: function(e) { e.preventDefault(); @@ -32,13 +33,13 @@ RhsHeaderSearch = React.createClass({ return ( <div className="sidebar--right__header"> <span className="sidebar--right__title">{title}</span> - <button type="button" className="sidebar--right__close" aria-label="Close" onClick={this.handleClose}></button> + <button type="button" className="sidebar--right__close" aria-label="Close" title="Close" onClick={this.handleClose}></button> </div> ); } }); -SearchItem = React.createClass({ +var SearchItem = React.createClass({ handleClick: function(e) { e.preventDefault(); @@ -62,39 +63,41 @@ SearchItem = React.createClass({ }); }, function(err) { - dispatchError(err, "getPost"); + AsyncClient.dispatchError(err, "getPost"); } ); - var postChannel = ChannelStore.get(this.props.post.channel_id); - var teammate = postChannel.type === 'D' ? utils.getDirectTeammate(this.props.post.channel_id).username : ""; + var postChannel = ChannelStore.get(this.props.post.channel_id); + var teammate = postChannel.type === 'D' ? utils.getDirectTeammate(this.props.post.channel_id).username : ""; - utils.switchChannel(postChannel,teammate); + utils.switchChannel(postChannel, teammate); }, + render: function() { 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) { - if (channel.type === 'D') { - channelName = "Private Message"; - } else { - channelName = channel.display_name; - } + channelName = (channel.type === 'D') ? "Private Message" : channel.display_name; } - return ( + return ( <div className="search-item-container post" onClick={this.handleClick}> <div className="search-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"} height="36" width="36" /> + <img className="post-profile-img" src={"/api/v1/users/" + this.props.post.user_id + "/image?time=" + timestamp} height="36" width="36" /> </div> <div className="post__content"> <ul className="post-header"> <li className="post-header-col"><strong><UserProfile userId={this.props.post.user_id} /></strong></li> - <li className="post-header-col"><time className="search-item-time">{ utils.displayDate(this.props.post.create_at)+' '+utils.displayTime(this.props.post.create_at) }</time></li> + <li className="post-header-col"> + <time className="search-item-time"> + { utils.displayDate(this.props.post.create_at) + ' ' + utils.displayTime(this.props.post.create_at) } + </time> + </li> </ul> <div className="search-item-snippet"><span>{message}</span></div> </div> @@ -103,11 +106,13 @@ SearchItem = React.createClass({ } }); + function getStateFromStores() { return { results: PostStore.getSearchResults() }; } module.exports = React.createClass({ + displayName: 'SearchResults', componentDidMount: function() { PostStore.addSearchChangeListener(this._onChange); this.resize(); @@ -143,41 +148,24 @@ module.exports = React.createClass({ var results = this.state.results; var currentId = UserStore.getCurrentId(); - var searchForm = currentId == null ? null : <SearchBox />; - - if (results == null) { - return ( - <div className="sidebar--right__header"> - <div className="sidebar__heading">Search Results</div> - </div> - ); - } + var searchForm = currentId ? <SearchBox /> : null; + var noResults = (!results || !results.order || !results.order.length); + var searchTerm = PostStore.getSearchTerm(); - if (!results.order || results.order.length == 0) { - return ( - <div className="sidebar--right__content"> - <div className="search-bar__container">{searchForm}</div> - <div className="sidebar-right__body"> - <RhsHeaderSearch /> - <div id="search-items-container" className="search-items-container"> - <div className="sidebar--right__subheader">No results</div> - </div> - </div> - </div> - ); - } - - var self = this; return ( <div className="sidebar--right__content"> <div className="search-bar__container sidebar--right__search-header">{searchForm}</div> <div className="sidebar-right__body"> <RhsHeaderSearch isMentionSearch={this.props.isMentionSearch} /> <div id="search-items-container" className="search-items-container"> - {results.order.map(function(id) { - var post = results.posts[id]; - return <SearchItem key={post.id} post={post} term={PostStore.getSearchTerm()} isMentionSearch={self.props.isMentionSearch} /> - })} + + { noResults ? <div className="sidebar--right__subheader">No results</div> + : results.order.map(function(id) { + var post = results.posts[id]; + return <SearchItem key={post.id} post={post} term={searchTerm} isMentionSearch={this.props.isMentionSearch} /> + }, this) + } + </div> </div> </div> diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 2095978e8..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(); @@ -131,7 +45,7 @@ function getStateFromStores() { var channel = ChannelStore.getByName(channelName); if (channel != null) { - channel.display_name = teammate.full_name.trim() != "" ? teammate.full_name : teammate.username; + channel.display_name = utils.getDisplayName(teammate); channel.teammate_username = teammate.username; channel.status = UserStore.getStatus(teammate.id); @@ -150,7 +64,7 @@ function getStateFromStores() { var tempChannel = {}; tempChannel.fake = true; tempChannel.name = channelName; - tempChannel.display_name = teammate.full_name.trim() != "" ? teammate.full_name : teammate.username; + tempChannel.display_name = utils.getDisplayName(teammate); tempChannel.status = UserStore.getStatus(teammate.id); tempChannel.last_post_at = 0; readDirectChannels.push(tempChannel); @@ -192,7 +106,7 @@ function getStateFromStores() { }; } -var SidebarLoggedIn = React.createClass({ +module.exports = React.createClass({ componentDidMount: function() { ChannelStore.addChangeListener(this._onChange); UserStore.addChangeListener(this._onChange); @@ -249,11 +163,27 @@ var SidebarLoggedIn = React.createClass({ var repRegex = new RegExp("<br>", "g"); var post = JSON.parse(msg.props.post); - var msg = post.message.replace(repRegex, "\n").split("\n")[0].replace("<mention>", "").replace("</mention>", ""); + var msgProps = msg.props; + var msg = post.message.replace(repRegex, "\n").replace(/\n+/g, " ").replace("<mention>", "").replace("</mention>", ""); + if (msg.length > 50) { msg = msg.substring(0,49) + "..."; } - utils.notifyMe(title, username + " wrote: " + msg, channel); + + if (msg.length === 0) { + if (msgProps.image) { + utils.notifyMe(title, username + " uploaded an image", channel); + } + else if (msgProps.otherFile) { + utils.notifyMe(title, username + " uploaded a file", channel); + } + else { + utils.notifyMe(title, username + " did something new", channel); + } + } + else { + utils.notifyMe(title, username + " wrote: " + msg, channel); + } if (!user.notify_props || user.notify_props.desktop_sound === "true") { utils.ding(); } @@ -367,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> ); } @@ -398,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"> @@ -424,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 0b59d2036..bab2897b6 100644 --- a/web/react/components/sidebar_header.jsx +++ b/web/react/components/sidebar_header.jsx @@ -37,17 +37,13 @@ var NavbarDropdown = React.createClass({ var invite_link = ""; var manage_link = ""; var rename_link = ""; - var currentUser = UserStore.getCurrentUser() + var currentUser = UserStore.getCurrentUser(); var isAdmin = false; if (currentUser != null) { isAdmin = currentUser.roles.indexOf("admin") > -1; - invite_link = ( - <li> - <a href="#" data-toggle="modal" data-target="#invite_member">Invite New Member</a> - </li> - ); + invite_link = ( <li> <a href="#" data-toggle="modal" data-target="#invite_member">Invite New Member</a> </li>); if (this.props.teamType == "O") { team_link = ( @@ -59,43 +55,31 @@ var NavbarDropdown = React.createClass({ } if (isAdmin) { - manage_link = ( - <li> - <a href="#" data-toggle="modal" data-target="#team_members">Manage Team</a> - </li> - ); - rename_link = ( - <li> - <a href="#" data-toggle="modal" data-target="#rename_team_link">Rename</a> - </li> - ); + manage_link = ( <li> <a href="#" data-toggle="modal" data-target="#team_members">Manage Team</a> </li>); + rename_link = ( <li> <a href="#" data-toggle="modal" data-target="#rename_team_link">Rename</a> </li>); } 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]; - - if (domain == utils.getSubDomain()) - continue; + var teamName = this.state.teams[i]; - 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"> <li className="dropdown"> <a href="#" className="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"> - <i className="dropdown__icon"></i> + <span className="dropdown__icon" dangerouslySetInnerHTML={{__html: " <svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='4px' height='16px' viewBox='0 0 8 32' enable-background='new 0 0 8 32' xml:space='preserve'> <g> <circle cx='4' cy='4.062' r='4'/> <circle cx='4' cy='16' r='4'/> <circle cx='4' cy='28' r='4'/> </g> </svg>"}} /> </a> <ul className="dropdown-menu" role="menu"> <li><a href="#" data-toggle="modal" data-target="#user_settings1">Account Settings</a></li> - { isAdmin ? <li><a href="#" data-toggle="modal" data-target="#team_settings">Team Settings</a></li> : "" } + { isAdmin ? <li><a href="#" data-toggle="modal" data-target="#team_settings">Team Settings</a></li> : null } { invite_link } { team_link } { manage_link } @@ -113,23 +97,32 @@ var NavbarDropdown = React.createClass({ }); module.exports = React.createClass({ - handleSubmit: function(e) { - e.preventDefault(); - }, - getInitialState: function() { - return { }; + displayName: 'SidebarHeader', + + getDefaultProps: function() { + return { + teamName: config.SiteName + }; }, + render: function() { - var teamName = this.props.teamName ? this.props.teamName : config.SiteName; + var teamDisplayName = this.props.teamDisplayName ? this.props.teamDisplayName : config.SiteName; + var me = UserStore.getCurrentUser() + if (!me) { + return null; + } return ( <div className="team__header theme"> - <a className="team__name" href="/channels/town-square">{ teamName }</a> + <a className="settings_link" href="#" data-toggle="modal" data-target="#user_settings1"> + <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> + <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 500ee231e..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> ); } @@ -596,28 +626,23 @@ PasswordPage = React.createClass({ module.exports = React.createClass({ updateParent: function(state, skipSet) { - BrowserStore.setGlobalItem(this.props.hash, JSON.stringify(state)); + BrowserStore.setGlobalItem(this.props.hash, state); if (!skipSet) { this.setState(state); } }, getInitialState: function() { - var props = null; - try { - props = JSON.parse(BrowserStore.getGlobalItem(this.props.hash)); - } - catch(parse_error) { - } + var props = BrowserStore.getGlobalItem(this.props.hash); if (!props) { props = {}; 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(""); @@ -628,19 +653,19 @@ module.exports = React.createClass({ props.data = this.props.data; } - return props ; + return props; }, render: function() { if (this.state.wizard == "welcome") { 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 fb96cc99f..eed323d1f 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -17,7 +17,7 @@ module.exports = React.createClass({ return; } - var username_error = utils.isValidUsername(this.state.user.username) + var username_error = utils.isValidUsername(this.state.user.username); if (username_error === "Cannot use a reserved word as a username.") { this.setState({name_error: "This username is reserved, please choose a new one.", email_error: "", password_error: "", server_error: ""}); return; @@ -46,25 +46,25 @@ module.exports = React.createClass({ function(data) { client.track('signup', 'signup_user_02_complete'); - if (data.email_verified) { - 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'; - }.bind(this), - function(err) { + client.loginByEmail(this.props.domain, this.state.user.email, this.state.user.password, + function(data) { + 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 = '/'; + }.bind(this), + function(err) { + if (err.message == "Login failed because email address has not been verified") { + 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); - }.bind(this) - ); - } - else { - window.location.href = "/verify?email="+ encodeURIComponent(this.state.user.email) + "&domain=" + encodeURIComponent(this.props.domain); - } + } + }.bind(this) + ); }.bind(this), function(err) { this.state.server_error = err.message; @@ -73,12 +73,7 @@ module.exports = React.createClass({ ); }, getInitialState: function() { - var props = null; - try { - props = JSON.parse(BrowserStore.getGlobalItem(this.props.hash)); - } - catch(parse_error) { - } + var props = BrowserStore.getGlobalItem(this.props.hash); if (!props) { props = {}; @@ -91,7 +86,7 @@ module.exports = React.createClass({ props.original_email = this.props.email; } - return props ; + return props; }, render: function() { 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_profile.jsx b/web/react/components/user_profile.jsx index 648960471..65f025919 100644 --- a/web/react/components/user_profile.jsx +++ b/web/react/components/user_profile.jsx @@ -53,7 +53,7 @@ module.exports = React.createClass({ var name = this.props.overwriteName ? this.props.overwriteName : this.state.profile.username; - var data_content = "<img style='margin: 10px' src='/api/v1/users/" + this.state.profile.id + "/image' height='128' width='128' />"; + var data_content = "<img class='user-popover__image' src='/api/v1/users/" + this.state.profile.id + "/image?time=" + this.state.profile.update_at + "' height='128' width='128' />"; if (!config.ShowEmail) { data_content += "<div class='text-nowrap'>Email not shared</div>"; } else { @@ -61,7 +61,7 @@ module.exports = React.createClass({ } return ( - <div style={{"cursor" : "pointer", "display" : "inline-block"}} className="user-popover" id={"profile_" + this.uniqueId} data-toggle="popover" data-content={data_content} data-original-title={this.state.profile.username} > + <div className="user-popover" id={"profile_" + this.uniqueId} data-toggle="popover" data-content={data_content} data-original-title={this.state.profile.username} > { name } </div> ); diff --git a/web/react/components/user_settings.jsx b/web/react/components/user_settings.jsx index b4c3747af..59c97c309 100644 --- a/web/react/components/user_settings.jsx +++ b/web/react/components/user_settings.jsx @@ -156,6 +156,8 @@ var NotificationsTab = React.createClass({ var self = this; + var user = this.props.user; + var desktopSection; if (this.props.activeSection === 'desktop') { var notifyActive = [false, false, false]; @@ -314,20 +316,14 @@ var NotificationsTab = React.createClass({ var keysSection; if (this.props.activeSection === 'keys') { - var user = this.props.user; - var first_name = ""; - if (user.full_name.length > 0) { - first_name = user.full_name.split(' ')[0]; - } - var inputs = []; - if (first_name != "") { + if (user.first_name) { inputs.push( <div> <div className="checkbox"> <label> - <input type="checkbox" checked={this.state.first_name_key} onChange={function(e){self.updateFirstNameKey(e.target.checked);}}>{'Your case sensitive first name "' + first_name + '"'}</input> + <input type="checkbox" checked={this.state.first_name_key} onChange={function(e){self.updateFirstNameKey(e.target.checked);}}>{'Your case sensitive first name "' + user.first_name + '"'}</input> </label> </div> </div> @@ -396,14 +392,9 @@ var NotificationsTab = React.createClass({ ); } else { var keys = []; - if (this.state.first_name_key) { - var first_name = ""; - var user = this.props.user; - if (user.full_name.length > 0) first_name = user.full_name.split(' ')[0]; - if (first_name != "") keys.push(first_name); - } - if (this.state.username_key) keys.push(this.props.user.username); - if (this.state.mention_key) keys.push('@'+this.props.user.username); + if (this.state.first_name_key) keys.push(user.first_name); + if (this.state.username_key) keys.push(user.username); + if (this.state.mention_key) keys.push('@'+user.username); if (this.state.all_key) keys.push('@all'); if (this.state.channel_key) keys.push('@channel'); if (this.state.custom_keys.length > 0) keys = keys.concat(this.state.custom_keys.split(',')); @@ -560,7 +551,7 @@ var AuditTab = React.createClass({ <div className="user-settings"> <h3 className="tab-header">Activity Log</h3> <div className="divider-dark first"/> - <div className="table-responsive" style={{ maxWidth: "560px", maxHeight: "300px" }}> + <div className="table-responsive"> <table className="table-condensed small"> <thead> <tr> @@ -576,11 +567,11 @@ var AuditTab = React.createClass({ this.state.audits.map(function(value, index) { return ( <tr key={ "" + index }> - <td style={{ whiteSpace: "nowrap" }}>{ new Date(value.create_at).toLocaleString() }</td> - <td style={{ whiteSpace: "nowrap" }}>{ value.action.replace("/api/v1", "") }</td> - <td style={{ whiteSpace: "nowrap" }}>{ value.ip_address }</td> - <td style={{ whiteSpace: "nowrap" }}>{ value.session_id }</td> - <td style={{ whiteSpace: "nowrap" }}>{ value.extra_info }</td> + <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) @@ -626,7 +617,7 @@ var SecurityTab = React.createClass({ client.updatePassword(data, function(data) { - this.updateSection(""); + this.props.updateSection(""); AsyncClient.getMe(); this.setState({ current_password: '', new_password: '', confirm_password: '' }); }.bind(this), @@ -752,6 +743,21 @@ var GeneralTab = React.createClass({ this.submitUser(user); }, + submitNickname: function(e) { + e.preventDefault(); + + var user = UserStore.getCurrentUser(); + var nickname = this.state.nickname.trim(); + + if (user.nickname === nickname) { + this.setState({client_error: "You must submit a new nickname"}) + return; + } + + user.nickname = nickname; + + this.submitUser(user); + }, submitName: function(e) { e.preventDefault(); @@ -759,14 +765,13 @@ var GeneralTab = React.createClass({ var firstName = this.state.first_name.trim(); var lastName = this.state.last_name.trim(); - var fullName = firstName + ' ' + lastName; - - if (user.full_name === fullName) { - this.setState({client_error: "You must submit a new name"}) + if (user.first_name === firstName && user.last_name === lastName) { + this.setState({client_error: "You must submit a new first or last name"}) return; } - user.full_name = fullName; + user.first_name = firstName; + user.last_name = lastName; this.submitUser(user); }, @@ -820,6 +825,7 @@ var GeneralTab = React.createClass({ client.uploadProfileImage(formData, function(data) { this.submitActive = false; + AsyncClient.getMe(); window.location.reload(); }.bind(this), function(err) { @@ -838,6 +844,9 @@ var GeneralTab = React.createClass({ updateLastName: function(e) { this.setState({ last_name: e.target.value}); }, + updateNickname: function(e) { + this.setState({nickname: e.target.value}); + }, updateEmail: function(e) { this.setState({ email: e.target.value}); }, @@ -860,11 +869,7 @@ var GeneralTab = React.createClass({ getInitialState: function() { var user = this.props.user; - var splitStr = user.full_name.split(' '); - var firstName = splitStr.shift(); - var lastName = splitStr.join(' '); - - return { username: user.username, first_name: firstName, last_name: lastName, + return { username: user.username, first_name: user.first_name, last_name: user.last_name, nickname: user.nickname, email: user.email, picture: null }; }, render: function() { @@ -900,7 +905,7 @@ var GeneralTab = React.createClass({ nameSection = ( <SettingItemMax - title="Name" + title="Full Name" inputs={inputs} submit={this.submitName} server_error={server_error} @@ -909,15 +914,58 @@ var GeneralTab = React.createClass({ /> ); } else { + var full_name = ""; + + if (user.first_name && user.last_name) { + full_name = user.first_name + " " + user.last_name; + } else if (user.first_name) { + full_name = user.first_name; + } else if (user.last_name) { + full_name = user.last_name; + } + nameSection = ( <SettingItemMin - title="Name" - describe={UserStore.getCurrentUser().full_name} + title="Full Name" + describe={full_name} updateSection={function(){self.updateSection("name");}} /> ); } + var nicknameSection; + if (this.props.activeSection === 'nickname') { + var inputs = []; + + inputs.push( + <div className="form-group"> + <label className="col-sm-5 control-label">{utils.isMobile() ? "": "Nickname"}</label> + <div className="col-sm-7"> + <input className="form-control" type="text" onChange={this.updateNickname} value={this.state.nickname}/> + </div> + </div> + ); + + nicknameSection = ( + <SettingItemMax + title="Nickname" + inputs={inputs} + submit={this.submitNickname} + server_error={server_error} + client_error={client_error} + updateSection={function(e){self.updateSection("");e.preventDefault();}} + /> + ); + } else { + nicknameSection = ( + <SettingItemMin + title="Nickname" + describe={UserStore.getCurrentUser().nickname} + updateSection={function(){self.updateSection("nickname");}} + /> + ); + } + var usernameSection; if (this.props.activeSection === 'username') { var inputs = []; @@ -989,7 +1037,7 @@ var GeneralTab = React.createClass({ <SettingPicture title="Profile Picture" submit={this.submitPicture} - src={"/api/v1/users/" + user.id + "/image"} + src={"/api/v1/users/" + user.id + "/image?time=" + user.last_picture_update} server_error={server_error} client_error={client_error} updateSection={function(e){self.updateSection("");e.preventDefault();}} @@ -1000,10 +1048,14 @@ var GeneralTab = React.createClass({ ); } else { + var minMessage = "Click Edit to upload an image."; + if (user.last_picture_update) { + minMessage = "Image last updated " + utils.displayDate(user.last_picture_update) + } pictureSection = ( <SettingItemMin title="Profile Picture" - describe="Picture inside." + describe={minMessage} updateSection={function(){self.updateSection("picture");}} /> ); @@ -1021,6 +1073,8 @@ var GeneralTab = React.createClass({ <div className="divider-light"/> {usernameSection} <div className="divider-light"/> + {nicknameSection} + <div className="divider-light"/> {emailSection} <div className="divider-light"/> {pictureSection} diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx index 38f439946..2274f3f2e 100644 --- a/web/react/components/view_image.jsx +++ b/web/react/components/view_image.jsx @@ -25,7 +25,11 @@ 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) { @@ -48,6 +52,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 +61,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); } } }, diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx index 3aa985863..f70d60e3a 100644 --- a/web/react/pages/channel.jsx +++ b/web/react/pages/channel.jsx @@ -61,17 +61,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 +91,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 +186,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') ); 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 82cf9a942..4eed754cc 100644 --- a/web/react/stores/browser_store.jsx +++ b/web/react/stores/browser_store.jsx @@ -1,85 +1,104 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var UserStore = require('../stores/user_store.jsx'); -// Also change model/utils.go ETAG_ROOT_VERSION -var BROWSER_STORE_VERSION = '.1'; - -var _initialized = false; - -function _initialize() { - var currentVersion = localStorage.getItem("local_storage_version"); - if (currentVersion !== BROWSER_STORE_VERSION) { - localStorage.clear(); - sessionStorage.clear(); - localStorage.setItem("local_storage_version", BROWSER_STORE_VERSION); - } - _initialized = true; +var UserStore; +function getPrefix() { + if (!UserStore) UserStore = require('./user_store.jsx'); + return UserStore.getCurrentId() + '_'; } -module.exports.setItem = function(name, value) { - if (!_initialized) _initialize(); - var user_id = UserStore.getCurrentId(); - localStorage.setItem(user_id + "_" + name, value); -}; +// Also change model/utils.go ETAG_ROOT_VERSION +var BROWSER_STORE_VERSION = '.4'; -module.exports.getItem = function(name) { - if (!_initialized) _initialize(); - var user_id = UserStore.getCurrentId(); - return localStorage.getItem(user_id + "_" + name); -}; +module.exports = { + _initialized: false, -module.exports.removeItem = function(name) { - if (!_initialized) _initialize(); - var user_id = UserStore.getCurrentId(); - localStorage.removeItem(user_id + "_" + name); -}; + _initialize: function() { + var currentVersion = localStorage.getItem("local_storage_version"); + if (currentVersion !== BROWSER_STORE_VERSION) { + this.clear(); + localStorage.setItem("local_storage_version", BROWSER_STORE_VERSION); + } + this._initialized = true; + }, -module.exports.setGlobalItem = function(name, value) { - if (!_initialized) _initialize(); - localStorage.setItem(name, value); -}; + getItem: function(name, defaultValue) { + return this.getGlobalItem(getPrefix() + name, defaultValue); + }, -module.exports.getGlobalItem = function(name) { - if (!_initialized) _initialize(); - return localStorage.getItem(name); -}; + setItem: function(name, value) { + this.setGlobalItem(getPrefix() + name, value); + }, -module.exports.removeGlobalItem = function(name) { - if (!_initialized) _initialize(); - localStorage.removeItem(name); -}; + removeItem: function(name) { + if (!this._initialized) this._initialize(); -module.exports.clear = function() { - localStorage.clear(); - sessionStorage.clear(); -}; + localStorage.removeItem(getPrefix() + name); + }, + + setGlobalItem: function(name, value) { + if (!this._initialized) this._initialize(); + + localStorage.setItem(name, JSON.stringify(value)); + }, + + getGlobalItem: function(name, defaultValue) { + if (!this._initialized) this._initialize(); -// Preforms the given action on each item that has the given prefix -// Signiture for action is action(key, value) -module.exports.actionOnItemsWithPrefix = function (prefix, action) { - var user_id = UserStore.getCurrentId(); - var id_len = user_id.length; - var prefix_len = prefix.length; - for (var key in localStorage) { - if (key.substring(id_len, id_len + prefix_len) === prefix) { - var userkey = key.substring(id_len); - action(userkey, BrowserStore.getItem(key)); + var result = null; + try { + result = JSON.parse(localStorage.getItem(name)); + } catch (err) {} + + if (result === null && typeof defaultValue !== 'undefined') { + result = defaultValue; } - } -}; -module.exports.isLocalStorageSupported = function() { - try { - sessionStorage.setItem("testSession", '1'); - sessionStorage.removeItem("testSession"); + return result; + }, - localStorage.setItem("testLocal", '1'); - localStorage.removeItem("testLocal", '1'); + removeGlobalItem: function(name) { + if (!this._initialized) this._initialize(); - return true; - } - catch (e) { - return false; + localStorage.removeItem(name); + }, + + clear: function() { + localStorage.clear(); + sessionStorage.clear(); + }, + + /** + * Preforms the given action on each item that has the given prefix + * Signiture for action is action(key, value) + */ + actionOnItemsWithPrefix: function (prefix, action) { + if (!this._initialized) this._initialize(); + + var globalPrefix = getPrefix(); + var globalPrefixiLen = globalPrefix.length; + for (var key in localStorage) { + if (key.lastIndexOf(globalPrefix + prefix, 0) === 0) { + var userkey = key.substring(globalPrefixiLen); + action(userkey, this.getGlobalItem(key)); + } + } + }, + + isLocalStorageSupported: function() { + try { + sessionStorage.setItem("testSession", '1'); + sessionStorage.removeItem("testSession"); + + localStorage.setItem("testLocal", '1'); + if (localStorage.getItem("testLocal") != '1') { + return false; + } + localStorage.removeItem("testLocal", '1'); + + return true; + } catch (e) { + return false; + } } }; diff --git a/web/react/stores/channel_store.jsx b/web/react/stores/channel_store.jsx index 4429a5312..a97f13391 100644 --- a/web/react/stores/channel_store.jsx +++ b/web/react/stores/channel_store.jsx @@ -16,6 +16,7 @@ var MORE_CHANGE_EVENT = 'change'; var EXTRA_INFO_EVENT = 'extra_info'; var ChannelStore = assign({}, EventEmitter.prototype, { + _current_id: null, emitChange: function() { this.emit(CHANGE_EVENT); }, @@ -43,40 +44,24 @@ var ChannelStore = assign({}, EventEmitter.prototype, { removeExtraInfoChangeListener: function(callback) { this.removeListener(EXTRA_INFO_EVENT, callback); }, - get: function(id) { - var current = null; - var c = this._getChannels(); - - c.some(function(channel) { - if (channel.id == id) { - current = channel; - return true; + findFirstBy: function(field, value) { + var channels = this._getChannels(); + for (var i = 0; i < channels.length; i++) { + if (channels[i][field] == value) { + return channels[i]; } - return false; - }); + } - return current; + return null; + }, + get: function(id) { + return this.findFirstBy('id', id); }, getMember: function(id) { - var current = null; return this.getAllMembers()[id]; }, getByName: function(name) { - var current = null; - var c = this._getChannels(); - - c.some(function(channel) { - if (channel.name == name) { - current = channel; - return true; - } - - return false; - - }); - - return current; - + return this.findFirstBy('name', name); }, getAll: function() { return this._getChannels(); @@ -88,10 +73,7 @@ var ChannelStore = assign({}, EventEmitter.prototype, { return this._getMoreChannels(); }, setCurrentId: function(id) { - if (id == null) - BrowserStore.removeItem("current_channel_id"); - else - BrowserStore.setItem("current_channel_id", id); + this._current_id = id; }, setLastVisitedName: function(name) { if (name == null) @@ -117,12 +99,12 @@ var ChannelStore = assign({}, EventEmitter.prototype, { this._storeChannelMembers(cm); }, getCurrentId: function() { - return BrowserStore.getItem("current_channel_id"); + return this._current_id; }, getCurrent: function() { - var currentId = ChannelStore.getCurrentId(); + var currentId = this.getCurrentId(); - if (currentId != null) + if (currentId) return this.get(currentId); else return null; @@ -130,7 +112,7 @@ var ChannelStore = assign({}, EventEmitter.prototype, { getCurrentMember: function() { var currentId = ChannelStore.getCurrentId(); - if (currentId != null) + if (currentId) return this.getAllMembers()[currentId]; else return null; @@ -145,7 +127,7 @@ var ChannelStore = assign({}, EventEmitter.prototype, { var currentId = ChannelStore.getCurrentId(); var extra = null; - if (currentId != null) + if (currentId) extra = this._getExtraInfos()[currentId]; if (extra == null) @@ -156,7 +138,7 @@ var ChannelStore = assign({}, EventEmitter.prototype, { getExtraInfo: function(channel_id) { var extra = null; - if (channel_id != null) + if (channel_id) extra = this._getExtraInfos()[channel_id]; if (extra == null) @@ -165,49 +147,22 @@ var ChannelStore = assign({}, EventEmitter.prototype, { return extra; }, _storeChannels: function(channels) { - BrowserStore.setItem("channels", JSON.stringify(channels)); + BrowserStore.setItem("channels", channels); }, _getChannels: function() { - var channels = []; - try { - channels = JSON.parse(BrowserStore.getItem("channels")); - } - catch (err) { - } - - if (channels == null) { - channels = []; - } - - return channels; + return BrowserStore.getItem("channels", []); }, _storeChannelMembers: function(channelMembers) { - BrowserStore.setItem("channel_members", JSON.stringify(channelMembers)); + BrowserStore.setItem("channel_members", channelMembers); }, _getChannelMembers: function() { - var members = {}; - try { - members = JSON.parse(BrowserStore.getItem("channel_members")); - } - catch (err) { - } - - if (members == null) { - members = {}; - } - - return members; + return BrowserStore.getItem("channel_members", {}); }, _storeMoreChannels: function(channels) { - BrowserStore.setItem("more_channels", JSON.stringify(channels)); + BrowserStore.setItem("more_channels", channels); }, _getMoreChannels: function() { - var channels = null; - try { - channels = JSON.parse(BrowserStore.getItem("more_channels")); - } - catch (err) { - } + var channels = BrowserStore.getItem("more_channels"); if (channels == null) { channels = {}; @@ -217,22 +172,14 @@ var ChannelStore = assign({}, EventEmitter.prototype, { return channels; }, _storeExtraInfos: function(extraInfos) { - BrowserStore.setItem("extra_infos", JSON.stringify(extraInfos)); + BrowserStore.setItem("extra_infos", extraInfos); }, _getExtraInfos: function() { - var members = {}; - try { - members = JSON.parse(BrowserStore.getItem("extra_infos")); - } - catch (err) { - } - - if (members == null) { - members = {}; - } - - return members; - } + return BrowserStore.getItem("extra_infos", {}); + }, + isDefault: function(channel) { + return channel.name == Constants.DEFAULT_CHANNEL; + } }); ChannelStore.dispatchToken = AppDispatcher.register(function(payload) { @@ -271,4 +218,4 @@ ChannelStore.dispatchToken = AppDispatcher.register(function(payload) { } }); -module.exports = ChannelStore; +module.exports = ChannelStore;
\ No newline at end of file diff --git a/web/react/stores/error_store.jsx b/web/react/stores/error_store.jsx index 3aed6aef2..203b692ec 100644 --- a/web/react/stores/error_store.jsx +++ b/web/react/stores/error_store.jsx @@ -29,18 +29,11 @@ var ErrorStore = assign({}, EventEmitter.prototype, { BrowserStore.removeItem("last_error"); }, getLastError: function() { - var error = null; - try { - error = JSON.parse(BrowserStore.getItem("last_error")); - } - catch (err) { - } - - return error; + return BrowserStore.getItem('last_error'); }, _storeLastError: function(error) { - BrowserStore.setItem("last_error", JSON.stringify(error)); + BrowserStore.setItem("last_error", error); }, }); diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx index 8bf3fdcb2..5280bfe08 100644 --- a/web/react/stores/post_store.jsx +++ b/web/react/stores/post_store.jsx @@ -106,55 +106,27 @@ var PostStore = assign({}, EventEmitter.prototype, { this.emitChange(); }, _storePosts: function(channelId, posts) { - BrowserStore.setItem("posts_" + channelId, JSON.stringify(posts)); + BrowserStore.setItem("posts_" + channelId, posts); }, getPosts: function(channelId) { - var posts = null; - try { - posts = JSON.parse(BrowserStore.getItem("posts_" + channelId)); - } - catch (err) { - } - - return posts; + return BrowserStore.getItem("posts_" + channelId); }, storeSearchResults: function(results, is_mention_search) { - BrowserStore.setItem("search_results", JSON.stringify(results)); + BrowserStore.setItem("search_results", results); is_mention_search = is_mention_search ? true : false; // force to bool - BrowserStore.setItem("is_mention_search", JSON.stringify(is_mention_search)); + BrowserStore.setItem("is_mention_search", is_mention_search); }, getSearchResults: function() { - var results = null; - try { - results = JSON.parse(BrowserStore.getItem("search_results")); - } - catch (err) { - } - - return results; + return BrowserStore.getItem("search_results"); }, getIsMentionSearch: function() { - var result = false; - try { - result = JSON.parse(BrowserStore.getItem("is_mention_search")); - } - catch (err) { - } - - return result; + return BrowserStore.getItem("is_mention_search"); }, storeSelectedPost: function(post_list) { - BrowserStore.setItem("select_post", JSON.stringify(post_list)); + BrowserStore.setItem("select_post", post_list); }, getSelectedPost: function() { - var post_list = null; - try { - post_list = JSON.parse(BrowserStore.getItem("select_post")); - } - catch (err) { - } - - return post_list; + return BrowserStore.getItem("select_post"); }, storeSearchTerm: function(term) { BrowserStore.setItem("search_term", term); @@ -165,27 +137,26 @@ var PostStore = assign({}, EventEmitter.prototype, { storeCurrentDraft: function(draft) { var channel_id = ChannelStore.getCurrentId(); var user_id = UserStore.getCurrentId(); - BrowserStore.setItem("draft_" + channel_id + "_" + user_id, JSON.stringify(draft)); + BrowserStore.setItem("draft_" + channel_id + "_" + user_id, draft); }, getCurrentDraft: function() { var channel_id = ChannelStore.getCurrentId(); var user_id = UserStore.getCurrentId(); - return JSON.parse(BrowserStore.getItem("draft_" + channel_id + "_" + user_id)); + return BrowserStore.getItem("draft_" + channel_id + "_" + user_id); }, storeDraft: function(channel_id, user_id, draft) { - BrowserStore.setItem("draft_" + channel_id + "_" + user_id, JSON.stringify(draft)); + BrowserStore.setItem("draft_" + channel_id + "_" + user_id, draft); }, getDraft: function(channel_id, user_id) { - return JSON.parse(BrowserStore.getItem("draft_" + channel_id + "_" + user_id)); + return BrowserStore.getItem("draft_" + channel_id + "_" + user_id); }, clearDraftUploads: function() { - BrowserStore.actionOnItemsWithPrefix("draft_", function (key, value) { - var d = JSON.parse(value); - if (d) { - d['uploadsInProgress'] = 0; - BrowserStore.setItem(key, JSON.stringify(d)); - } - }); + BrowserStore.actionOnItemsWithPrefix("draft_", function (key, value) { + if (value) { + value.uploadsInProgress = 0; + BrowserStore.setItem(key, value); + } + }); } }); diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index 39800ead5..8ebb854c9 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -10,8 +10,6 @@ var client = require('../utils/client.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; -var BrowserStore = require('../stores/browser_store.jsx'); - var CHANGE_EVENT = 'change'; var conn; diff --git a/web/react/stores/team_store.jsx b/web/react/stores/team_store.jsx index c494cb5b5..3f12725f8 100644 --- a/web/react/stores/team_store.jsx +++ b/web/react/stores/team_store.jsx @@ -57,28 +57,19 @@ 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", JSON.stringify(teams)); + BrowserStore.setItem("user_teams", teams); }, _getTeams: function() { - var teams = {}; - - try { - teams = JSON.parse(BrowserStore.getItem("user_teams")); - } - catch (err) { - } - - if (teams == null) { - teams = {}; - } - - return teams; + return BrowserStore.getItem("user_teams", {}); } }); diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx index e832b34c7..d03016c5d 100644 --- a/web/react/stores/user_store.jsx +++ b/web/react/stores/user_store.jsx @@ -8,7 +8,7 @@ var client = require('../utils/client.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; -var BrowserStore = require('../stores/browser_store.jsx'); +var BrowserStore = require('./browser_store.jsx'); var CHANGE_EVENT = 'change'; var CHANGE_EVENT_SESSIONS = 'change_sessions'; @@ -18,6 +18,8 @@ var CHANGE_EVENT_STATUSES = 'change_statuses'; var UserStore = assign({}, EventEmitter.prototype, { + _current_id: null, + emitChange: function(userId) { this.emit(CHANGE_EVENT, userId); }, @@ -64,13 +66,19 @@ var UserStore = assign({}, EventEmitter.prototype, { this.removeListener(CHANGE_EVENT_STATUSES, callback); }, setCurrentId: function(id) { - if (id == null) - BrowserStore.removeGlobalItem("current_user_id"); - else - BrowserStore.setGlobalItem("current_user_id", 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 = BrowserStore.getGlobalItem("current_user_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 @@ -93,25 +101,11 @@ var UserStore = assign({}, EventEmitter.prototype, { return this._getProfiles()[this.getCurrentId()]; }, setCurrentUser: function(user) { - this.saveProfile(user); this.setCurrentId(user.id); - }, - getLastDomain: function() { - var last_domain = BrowserStore.getItem("last_domain"); - if (last_domain == null) { - last_domain = ""; - } - return last_domain; - }, - setLastDomain: function(domain) { - BrowserStore.setItem("last_domain", domain); + this.saveProfile(user); }, getLastEmail: function() { - var last_email = BrowserStore.getItem("last_email"); - if (last_email == null) { - last_email = ""; - } - return last_email; + return BrowserStore.getItem("last_email", ''); }, setLastEmail: function(email) { BrowserStore.setItem("last_email", email); @@ -153,116 +147,51 @@ var UserStore = assign({}, EventEmitter.prototype, { this._storeProfiles(ps); }, _storeProfiles: function(profiles) { - BrowserStore.setGlobalItem("profiles", JSON.stringify(profiles)); + BrowserStore.setItem("profiles", profiles); var profileUsernameMap = {}; for (var id in profiles) { profileUsernameMap[profiles[id].username] = profiles[id]; } - BrowserStore.setGlobalItem("profileUsernameMap", JSON.stringify(profileUsernameMap)); + BrowserStore.setItem("profileUsernameMap", profileUsernameMap); }, _getProfiles: function() { - var profiles = {}; - try { - profiles = JSON.parse(BrowserStore.getGlobalItem("profiles")); - } - catch (err) { - } - - if (profiles == null) { - profiles = {}; - } - - return profiles; + return BrowserStore.getItem("profiles", {}); }, _getProfilesUsernameMap: function() { - var profileUsernameMap = {}; - try { - profileUsernameMap = JSON.parse(BrowserStore.getGlobalItem("profileUsernameMap")); - } - catch (err) { - } - - if (profileUsernameMap == null) { - profileUsernameMap = {}; - } - - return profileUsernameMap; + return BrowserStore.getItem("profileUsernameMap", {}); }, setSessions: function(sessions) { - BrowserStore.setItem("sessions", JSON.stringify(sessions)); + BrowserStore.setItem("sessions", sessions); }, getSessions: function() { - var sessions = []; - try { - sessions = JSON.parse(BrowserStore.getItem("sessions")); - } - catch (err) { - } - if (sessions == null) { - sessions = []; - } - - return sessions; + return BrowserStore.getItem("sessions", []); }, setAudits: function(audits) { - BrowserStore.setItem("audits", JSON.stringify(audits)); + BrowserStore.setItem("audits", audits); }, getAudits: function() { - var audits = []; - try { - audits = JSON.parse(BrowserStore.getItem("audits")); - } - catch (err) { - } - - if (audits == null) { - audits = []; - } - - return audits; + return BrowserStore.getItem("audits", []); }, setTeams: function(teams) { - BrowserStore.setItem("teams", JSON.stringify(teams)); + BrowserStore.setItem("teams", teams); }, getTeams: function() { - var teams = []; - try { - teams = JSON.parse(BrowserStore.getItem("teams")); - - } - catch (err) { - } - - if (teams == null) { - teams = []; - } - - return teams; + return BrowserStore.getItem("teams", []); }, getCurrentMentionKeys: function() { var user = this.getCurrentUser(); - if (user.notify_props && user.notify_props.mention_keys) { - var keys = user.notify_props.mention_keys.split(','); - if (user.full_name.length > 0 && user.notify_props.first_name === "true") { - var first = user.full_name.split(' ')[0]; - if (first.length > 0) keys.push(first); - } + var keys = []; - if (user.notify_props.all === "true") keys.push('@all'); - if (user.notify_props.channel === "true") keys.push('@channel'); + if (user.notify_props && user.notify_props.mention_keys) keys = keys.concat(user.notify_props.mention_keys.split(',')); + if (user.first_name && user.notify_props.first_name === "true") keys.push(user.first_name); + if (user.notify_props.all === "true") keys.push('@all'); + if (user.notify_props.channel === "true") keys.push('@channel'); - return keys; - } else { - return []; - } + return keys; }, getLastVersion: function() { - var last_version = BrowserStore.getItem("last_version"); - if (last_version == null) { - last_version = ""; - } - return last_version; + return BrowserStore.getItem("last_version", ''); }, setLastVersion: function(version) { BrowserStore.setItem("last_version", version); @@ -272,7 +201,7 @@ var UserStore = assign({}, EventEmitter.prototype, { this.emitStatusesChange(); }, _setStatuses: function(statuses) { - BrowserStore.setItem("statuses", JSON.stringify(statuses)); + BrowserStore.setItem("statuses", statuses); }, setStatus: function(user_id, status) { var statuses = this.getStatuses(); @@ -281,18 +210,7 @@ var UserStore = assign({}, EventEmitter.prototype, { this.emitStatusesChange(); }, getStatuses: function() { - var statuses = {}; - try { - statuses = JSON.parse(BrowserStore.getItem("statuses")); - } - catch (err) { - } - - if (statuses == null) { - statuses = {}; - } - - return statuses; + return BrowserStore.getItem("statuses", {}); }, getStatus: function(id) { return this.getStatuses()[id]; @@ -341,4 +259,3 @@ UserStore.setMaxListeners(0); global.window.UserStore = UserStore; module.exports = UserStore; - 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..187e3c4a3 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -54,7 +54,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 5ded0e76f..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(){ @@ -198,7 +198,13 @@ module.exports.getTimestamp = function() { } var testUrlMatch = function(text) { - var urlMatcher = new Autolinker.matchParser.MatchParser; + var urlMatcher = new Autolinker.matchParser.MatchParser({ + urls: true, + emails: false, + twitter: false, + phone: false, + hashtag: false, + }); var result = []; var replaceFn = function(match) { var linkData = {}; @@ -225,7 +231,7 @@ module.exports.extractLinks = function(text) { } return { "links": links, "text": text }; -} +} module.exports.escapeRegExp = function(string) { return string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); @@ -303,18 +309,25 @@ var getYoutubeEmbed = function(link) { }; var success = function(data) { - $('.video-uploader.'+youtubeId).html(data.data.uploader); - $('.video-title.'+youtubeId).find('a').html(data.data.title); + if(!data.items.length || !data.items[0].snippet) { + return; + } + var metadata = data.items[0].snippet; + $('.video-uploader.'+youtubeId).html(metadata.channelTitle); + $('.video-title.'+youtubeId).find('a').html(metadata.title); $(".post-list-holder-by-time").scrollTop($(".post-list-holder-by-time")[0].scrollHeight); $(".post-list-holder-by-time").perfectScrollbar('update'); }; - $.ajax({ - async: true, - url: 'https://gdata.youtube.com/feeds/api/videos/'+youtubeId+'?v=2&alt=jsonc', - type: 'GET', - success: success - }); + if(config.GoogleDeveloperKey) { + $.ajax({ + async: true, + url: "https://www.googleapis.com/youtube/v3/videos", + type: 'GET', + data: {part:"snippet", id:youtubeId, key:config.GoogleDeveloperKey}, + success: success + }); + } return ( <div className="post-comment"> @@ -671,13 +684,13 @@ module.exports.isValidUsername = function (name) { error = "First character must be a letter."; } - else + else { var lowerName = name.toLowerCase().trim(); - for (var i = 0; i < Constants.RESERVED_USERNAMES.length; i++) + for (var i = 0; i < Constants.RESERVED_USERNAMES.length; i++) { - if (lowerName === Constants.RESERVED_USERNAMES[i]) + if (lowerName === Constants.RESERVED_USERNAMES[i]) { error = "Cannot use a reserved word as a username."; break; @@ -695,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("-")); @@ -771,14 +784,57 @@ 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; + + if (col[0] == "#") { + col = col.slice(1); + usePound = true; + } + + var num = parseInt(col,16); + + var r = (num >> 16) + amt; + + if (r > 255) r = 255; + else if (r < 0) r = 0; + + var b = ((num >> 8) & 0x00FF) + amt; + + if (b > 255) b = 255; + else if (b < 0) b = 0; + + var g = (num & 0x0000FF) + amt; + + if (g > 255) g = 255; + else if (g < 0) g = 0; + + return (usePound?"#":"") + String("000000" + (g | (b << 8) | (r << 16)).toString(16)).slice(-6); +}; + +module.exports.getFullName = function(user) { + if (user.first_name && user.last_name) { + return user.first_name + " " + user.last_name; + } else if (user.first_name) { + return user.first_name; + } else if (user.last_name) { + return user.last_name; + } else { + return ""; + } +}; + +module.exports.getDisplayName = function(user) { + if (user.nickname && user.nickname.trim().length > 0) { + return user.nickname; + } else { + var fullName = module.exports.getFullName(user); + + if (fullName) { + return fullName; + } else { + return user.username; + } + } +}; |