diff options
37 files changed, 1338 insertions, 1121 deletions
@@ -101,14 +101,6 @@ install: docker start mattermost-mysql > /dev/null; \ fi - @if [ $(shell docker ps -a | grep -ci mattermost-redis) -eq 0 ]; then \ - echo starting mattermost-redis; \ - docker run --name mattermost-redis -p 6379:6379 -d redis > /dev/null; \ - elif [ $(shell docker ps | grep -ci mattermost-redis) -eq 0 ]; then \ - echo restarting mattermost-redis; \ - docker start mattermost-redis > /dev/null; \ - fi - @cd web/react/ && npm install check: install @@ -159,12 +151,6 @@ clean: docker rm -v mattermost-mysql > /dev/null; \ fi - @if [ $(shell docker ps -a | grep -ci mattermost-redis) -eq 1 ]; then \ - echo stopping mattermost-redis; \ - docker stop mattermost-redis > /dev/null; \ - docker rm -v mattermost-redis > /dev/null; \ - fi - rm -rf web/react/node_modules rm -f web/static/js/bundle*.js rm -f web/static/css/styles.css @@ -216,12 +202,6 @@ cleandb: docker stop mattermost-mysql > /dev/null; \ docker rm -v mattermost-mysql > /dev/null; \ fi - - @if [ $(shell docker ps -a | grep -ci mattermost-redis) -eq 1 ]; then \ - docker stop mattermost-redis > /dev/null; \ - docker rm -v mattermost-redis > /dev/null; \ - fi - dist: install @$(GO) build $(GOFLAGS) -i -a ./... @@ -31,12 +31,11 @@ Local Machine Setup (Docker) ### Mac OSX ### -1. Follow the instructions at: http://docs.docker.com/installation/mac/ - 1. Use the Boot2Docker command-line utility. - 2. If you do command-line setup use: `boot2docker init eval “$(boot2docker shellinit)”` +1. Install Boot2Docker using instructions at: http://docs.docker.com/installation/mac/ + 1. Start Boot2Docker from the command line and run: `boot2docker init eval “$(boot2docker shellinit)”` 2. Get your Docker IP address with: `boot2docker ip` -3. Add a line to your /etc/hosts that goes: `<Docker IP> dockerhost` -4. Run: `boot2docker shellinit` and copy the export statements to your ~/.bash\_profile. +3. Use `sudo nano /etc/hosts` to add `<Docker IP> dockerhost` to your /etc/hosts file +4. Run: `boot2docker shellinit` and copy the export statements to your ~/.bash\_profile by running `sudo nano ~/.bash_profile`. Then run: `source ~/.bash_profile` 5. Run: `docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform` 6. When docker is done fetching the image, open http://dockerhost:8065/ in your browser. diff --git a/STYLE-GUIDE.md b/STYLE-GUIDE.md index e3fe2addf..0da0a14f8 100644 --- a/STYLE-GUIDE.md +++ b/STYLE-GUIDE.md @@ -1,6 +1,6 @@ # Mattermost Style Guide -1. [GO](#go) +1. [Go](#go) 2. [Javascript](#javascript) 3. [React-JSX](#react-jsx) @@ -159,7 +159,7 @@ This is an abridged version of the [Airbnb React/JSX Style Guide](https://github - Property names use camelCase. - React component names use CapitalCamelCase. -- Do not use an understore for internal methods in a react component. +- Do not use an underscore for internal methods in a react component. ```xml // Correct diff --git a/api/file.go b/api/file.go index 4ec421eb9..bf1c59422 100644 --- a/api/file.go +++ b/api/file.go @@ -15,6 +15,8 @@ import ( "github.com/nfnt/resize" _ "golang.org/x/image/bmp" "image" + "image/color" + "image/draw" _ "image/gif" "image/jpeg" "io" @@ -138,6 +140,12 @@ func fireAndForgetHandleImages(filenames []string, fileData [][]byte, teamId, ch return } + // Remove transparency due to JPEG's lack of support of it + temp := image.NewRGBA(img.Bounds()) + draw.Draw(temp, temp.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src) + draw.Draw(temp, temp.Bounds(), img, img.Bounds().Min, draw.Over) + img = temp + // Create thumbnail go func() { thumbWidth := float64(utils.Cfg.ImageSettings.ThumbnailWidth) diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx index 76dbe370b..90a776791 100644 --- a/web/react/components/channel_header.jsx +++ b/web/react/components/channel_header.jsx @@ -1,13 +1,11 @@ // 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'); -var SocketStore = require('../stores/socket_store.jsx') -var UserProfile = require( './user_profile.jsx' ); -var NavbarSearchBox =require('./search_bar.jsx'); +var SocketStore = require('../stores/socket_store.jsx'); +var NavbarSearchBox = require('./search_bar.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var Client = require('../utils/client.jsx'); var utils = require('../utils/utils.jsx'); @@ -21,23 +19,28 @@ var PopoverListMembers = React.createClass({ componentDidMount: function() { var originalLeave = $.fn.popover.Constructor.prototype.leave; $.fn.popover.Constructor.prototype.leave = function(obj) { - var self = obj instanceof this.constructor ? obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type); + var selfObj; + if (obj instanceof this.constructor) { + selfObj = obj; + } else { + selfObj = $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type); + } originalLeave.call(this, obj); - if (obj.currentTarget && self.$tip) { - self.$tip.one('mouseenter', function() { - clearTimeout(self.timeout); - self.$tip.one('mouseleave', function() { - $.fn.popover.Constructor.prototype.leave.call(self, self); + if (obj.currentTarget && selfObj.$tip) { + selfObj.$tip.one('mouseenter', function() { + clearTimeout(selfObj.timeout); + selfObj.$tip.one('mouseleave', function() { + $.fn.popover.Constructor.prototype.leave.call(selfObj, selfObj); }); - }) + }); } }; - $("#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) { - $("#member_popover").popover('hide'); + $('#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) { + $('#member_popover').popover('hide'); } }); }, @@ -45,22 +48,27 @@ var PopoverListMembers = React.createClass({ render: function() { var popoverHtml = ''; var members = this.props.members; - var count = (members.length > 20) ? "20+" : (members.length || '-'); + var count; + if (members.length > 20) { + count = '20+'; + } else { + count = members.length || '-'; + } if (members) { - members.sort(function(a,b) { + members.sort(function(a, b) { return a.username.localeCompare(b.username); }); members.forEach(function(m) { - popoverHtml += "<div class='text--nowrap'>" + m.username + "</div>"; + popoverHtml += "<div class='text--nowrap'>" + m.username + '</div>'; }); } return ( - <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 id='member_popover' data-toggle='popover' data-content={popoverHtml} data-original-title='Members' > + <div id='member_tooltip' data-placement='left' data-toggle='tooltip' title='View Channel Members'> + {count} <span className='glyphicon glyphicon-user' aria-hidden='true'></span> </div> </div> ); @@ -68,53 +76,53 @@ var PopoverListMembers = React.createClass({ }); function getStateFromStores() { - return { - channel: ChannelStore.getCurrent(), - memberChannel: ChannelStore.getCurrentMember(), - memberTeam: UserStore.getCurrentUser(), - users: ChannelStore.getCurrentExtraInfo().members, - search_visible: PostStore.getSearchResults() != null - }; + return { + channel: ChannelStore.getCurrent(), + memberChannel: ChannelStore.getCurrentMember(), + memberTeam: UserStore.getCurrentUser(), + users: ChannelStore.getCurrentExtraInfo().members, + searchVisible: PostStore.getSearchResults() != null + }; } module.exports = React.createClass({ displayName: 'ChannelHeader', componentDidMount: function() { - ChannelStore.addChangeListener(this._onChange); - ChannelStore.addExtraInfoChangeListener(this._onChange); - PostStore.addSearchChangeListener(this._onChange); - UserStore.addChangeListener(this._onChange); - SocketStore.addChangeListener(this._onSocketChange); + ChannelStore.addChangeListener(this.onListenerChange); + ChannelStore.addExtraInfoChangeListener(this.onListenerChange); + PostStore.addSearchChangeListener(this.onListenerChange); + UserStore.addChangeListener(this.onListenerChange); + SocketStore.addChangeListener(this.onSocketChange); }, componentWillUnmount: function() { - ChannelStore.removeChangeListener(this._onChange); - ChannelStore.removeExtraInfoChangeListener(this._onChange); - PostStore.removeSearchChangeListener(this._onChange); - UserStore.addChangeListener(this._onChange); + ChannelStore.removeChangeListener(this.onListenerChange); + ChannelStore.removeExtraInfoChangeListener(this.onListenerChange); + PostStore.removeSearchChangeListener(this.onListenerChange); + UserStore.addChangeListener(this.onListenerChange); }, - _onChange: function() { + onListenerChange: function() { var newState = getStateFromStores(); if (!utils.areStatesEqual(newState, this.state)) { this.setState(newState); } - $(".channel-header__info .description").popover({placement : 'bottom', trigger: 'hover', html: true, delay: {show: 500, hide: 500}}); + $('.channel-header__info .description').popover({placement: 'bottom', trigger: 'hover', html: true, delay: {show: 500, hide: 500}}); }, - _onSocketChange: function(msg) { - if (msg.action === "new_user") { + onSocketChange: function(msg) { + if (msg.action === 'new_user') { AsyncClient.getChannelExtraInfo(true); } }, getInitialState: function() { return getStateFromStores(); }, - handleLeave: function(e) { + handleLeave: function() { Client.leaveChannel(this.state.channel.id, - function(data) { + function() { var townsquare = ChannelStore.getByName('town-square'); utils.switchChannel(townsquare); }, function(err) { - AsyncClient.dispatchError(err, "handleLeave"); + AsyncClient.dispatchError(err, 'handleLeave'); } ); }, @@ -123,9 +131,16 @@ module.exports = React.createClass({ var user = UserStore.getCurrentUser(); - var terms = ""; + var terms = ''; if (user.notify_props && user.notify_props.mention_keys) { - terms = UserStore.getCurrentMentionKeys().join(' '); + var termKeys = UserStore.getCurrentMentionKeys(); + if (user.notify_props.all === 'true' && termKeys.indexOf('@all') !== -1) { + termKeys.splice(termKeys.indexOf('@all'), 1); + } + if (user.notify_props.channel === 'true' && termKeys.indexOf('@channel') !== -1) { + termKeys.splice(termKeys.indexOf('@channel'), 1); + } + terms = termKeys.join(' '); } AppDispatcher.handleServerAction({ @@ -135,81 +150,89 @@ module.exports = React.createClass({ is_mention_search: true }); }, - render: function() { - if (this.state.channel == null) { return null; } var channel = this.state.channel; - var description = utils.textToJsx(channel.description, {"singleline": true, "noMentionHighlight": true}); + 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 isAdmin = this.state.memberChannel.roles.indexOf('admin') > -1 || this.state.memberTeam.roles.indexOf('admin') > -1; var isDirect = (this.state.channel.type === 'D'); if (isDirect) { if (this.state.users.length > 1) { - var contact = this.state.users[((this.state.users[0].id === currentId) ? 1 : 0)]; + var contact; + if (this.state.users[0].id === currentId) { + contact = this.state.users[1]; + } else { + contact = this.state.users[0]; + } channelTitle = contact.nickname || contact.username; } } + var channelTerm = 'Channel'; + if (channel.type === 'P') { + channelTerm = 'Group'; + } + return ( - <table className="channel-header alt"> + <table className='channel-header alt'> <tr> <th> - <div className="channel-header__info"> - <div className="dropdown"> - <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> + <div className='channel-header__info'> + <div className='dropdown'> + <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> - { !isDirect ? - <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={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> + {!isDirect ? + <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={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> + {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> + <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 {channelTerm} 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 {channelTerm}...</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> + {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 {channelTerm}...</a></li> : null } - { !ChannelStore.isDefault(channel) ? - <li role="presentation"><a role="menuitem" href="#" onClick={this.handleLeave}>Leave Channel</a></li> + {!ChannelStore.isDefault(channel) ? + <li role='presentation'><a role='menuitem' href='#' onClick={this.handleLeave}>Leave {channelTerm}</a></li> : null } </ul> : - <ul className="dropdown-menu" role="menu" aria-labelledby="channel_header_dropdown"> - <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> + <ul className='dropdown-menu' role='menu' aria-labelledby='channel_header_dropdown'> + <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> </ul> } </div> - <div data-toggle="popover" data-content={popoverContent} className="description">{description}</div> + <div data-toggle='popover' data-content={popoverContent} className='description'>{description}</div> </div> </th> <th><PopoverListMembers members={this.state.users} channelId={channel.id} /></th> - <th className="search-bar__container"><NavbarSearchBox /></th> + <th className='search-bar__container'><NavbarSearchBox /></th> <th> - <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"> - <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON }} /> </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> + <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'> + <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} /> </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> </th> diff --git a/web/react/components/delete_channel_modal.jsx b/web/react/components/delete_channel_modal.jsx index 64ceec450..589737271 100644 --- a/web/react/components/delete_channel_modal.jsx +++ b/web/react/components/delete_channel_modal.jsx @@ -34,7 +34,7 @@ module.exports = React.createClass({ var channelType = ChannelStore.getCurrent() && ChannelStore.getCurrent().type === 'P' ? "private group" : "channel" return ( - <div className="modal fade" ref="modal" id="delete_channel" role="dialog" aria-hidden="true"> + <div className="modal fade" ref="modal" id="delete_channel" role="dialog" tabIndex="-1" aria-hidden="true"> <div className="modal-dialog"> <div className="modal-content"> <div className="modal-header"> diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx index f0cb809af..1b6a7e162 100644 --- a/web/react/components/delete_post_modal.jsx +++ b/web/react/components/delete_post_modal.jsx @@ -82,7 +82,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; return ( - <div className="modal fade" id="delete_post" ref="modal" role="dialog" aria-hidden="true"> + <div className="modal fade" id="delete_post" ref="modal" role="dialog" tabIndex="-1" aria-hidden="true"> <div className="modal-dialog modal-push-down"> <div className="modal-content"> <div className="modal-header"> diff --git a/web/react/components/edit_channel_modal.jsx b/web/react/components/edit_channel_modal.jsx index 1b0cc185f..06d7fc3e8 100644 --- a/web/react/components/edit_channel_modal.jsx +++ b/web/react/components/edit_channel_modal.jsx @@ -51,7 +51,7 @@ module.exports = React.createClass({ var server_error = this.state.server_error ? <div className='form-group has-error'><br/><label className='control-label'>{ this.state.server_error }</label></div> : null; return ( - <div className="modal fade" ref="modal" id="edit_channel" role="dialog" aria-hidden="true"> + <div className="modal fade" ref="modal" id="edit_channel" role="dialog" tabIndex="-1" aria-hidden="true"> <div className="modal-dialog"> <div className="modal-content"> <div className="modal-header"> diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx index 21b75bb6e..064d3fa94 100644 --- a/web/react/components/edit_post_modal.jsx +++ b/web/react/components/edit_post_modal.jsx @@ -71,7 +71,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; return ( - <div className="modal fade edit-modal" ref="modal" id="edit_post" role="dialog" aria-hidden="true"> + <div className="modal fade edit-modal" ref="modal" id="edit_post" role="dialog" tabIndex="-1" aria-hidden="true"> <div className="modal-dialog modal-push-down"> <div className="modal-content"> <div className="modal-header"> diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx index af5314e64..ea22ad0f3 100644 --- a/web/react/components/get_link_modal.jsx +++ b/web/react/components/get_link_modal.jsx @@ -10,46 +10,57 @@ ZeroClipboardMixin.ZeroClipboard.config({ module.exports = React.createClass({ zeroclipboardElementsSelector: '[data-copy-btn]', - mixins: [ ZeroClipboardMixin ], + mixins: [ZeroClipboardMixin], componentDidMount: function() { var self = this; - if(this.refs.modal) { + if (this.refs.modal) { $(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) { var button = e.relatedTarget; - self.setState({title: $(button).attr('data-title'), value: $(button).attr('data-value') }); + self.setState({title: $(button).attr('data-title'), value: $(button).attr('data-value')}); + }); + $(this.refs.modal.getDOMNode()).on('hide.bs.modal', function() { + self.setState({copiedLink: false}); }); } }, getInitialState: function() { - return { }; + return {copiedLink: false}; + }, + handleClick: function() { + this.setState({copiedLink: true}); }, render: function() { - var currentUser = UserStore.getCurrentUser() + var currentUser = UserStore.getCurrentUser(); + var copyLinkConfirm = null; + + if (this.state.copiedLink) { + copyLinkConfirm = <p className='copy-link-confirm'>Link copied to clipboard.</p>; + } if (currentUser != null) { return ( - <div className="modal fade" ref="modal" id="get_link" tabIndex="-1" role="dialog" aria-hidden="true"> - <div className="modal-dialog"> - <div className="modal-content"> - <div className="modal-header"> - <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> - <h4 className="modal-title" id="myModalLabel">{this.state.title} Link</h4> + <div className='modal fade' ref='modal' id='get_link' tabIndex='-1' role='dialog' aria-hidden='true'> + <div className='modal-dialog'> + <div className='modal-content'> + <div className='modal-header'> + <button type='button' className='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>×</span></button> + <h4 className='modal-title' id='myModalLabel'>{this.state.title} Link</h4> </div> - <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."} + <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 no-resize" 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> - <button data-copy-btn type="button" className="btn btn-primary pull-left" data-clipboard-text={this.state.value}>Copy Link</button> + <div className='modal-footer'> + <button type='button' className='btn btn-default' data-dismiss='modal'>Close</button> + <button data-copy-btn='true' type='button' className='btn btn-primary pull-left' onClick={this.handleClick} data-clipboard-text={this.state.value}>Copy Link</button> + {copyLinkConfirm} </div> </div> </div> </div> ); - } else { - return <div/>; } + return <div/>; } }); diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx index fed96b50a..3eca79bae 100644 --- a/web/react/components/invite_member_modal.jsx +++ b/web/react/components/invite_member_modal.jsx @@ -2,7 +2,7 @@ // See License.txt for license information. var utils = require('../utils/utils.jsx'); -var Client =require('../utils/client.jsx'); +var Client = require('../utils/client.jsx'); var UserStore = require('../stores/user_store.jsx'); var ConfirmModal = require('./confirm_modal.jsx'); @@ -15,20 +15,19 @@ module.exports = React.createClass({ return; } - var not_empty = false; - for (var i = 0; i < self.state.invite_ids.length; i++) { - var index = self.state.invite_ids[i]; - if (self.refs["email"+index].getDOMNode().value.trim() !== '') { - not_empty = true; + var notEmpty = false; + for (var i = 0; i < self.state.inviteIds.length; i++) { + var index = self.state.inviteIds[i]; + if (self.refs['email' + index].getDOMNode().value.trim() !== '') { + notEmpty = true; break; } } - if (not_empty) { + if (notEmpty) { $('#confirm_invite_modal').modal('show'); e.preventDefault(); } - }); $('#invite_member').on('hidden.bs.modal', function() { @@ -36,52 +35,54 @@ module.exports = React.createClass({ }); }, handleSubmit: function(e) { - var invite_ids = this.state.invite_ids; - var count = invite_ids.length; + var inviteIds = this.state.inviteIds; + var count = inviteIds.length; var invites = []; - var email_errors = this.state.email_errors; - var first_name_errors = this.state.first_name_errors; - var last_name_errors = this.state.last_name_errors; + var emailErrors = this.state.emailErrors; + var firstNameErrors = this.state.firstNameErrors; + var lastNameErrors = this.state.lastNameErrors; var valid = true; for (var i = 0; i < count; i++) { - var index = invite_ids[i]; + var index = inviteIds[i]; var invite = {}; - invite.email = this.refs["email"+index].getDOMNode().value.trim(); + invite.email = this.refs['email' + index].getDOMNode().value.trim(); if (!invite.email || !utils.isEmail(invite.email)) { - email_errors[index] = "Please enter a valid email address"; + emailErrors[index] = 'Please enter a valid email address'; valid = false; } else { - email_errors[index] = ""; + emailErrors[index] = ''; } if (config.AllowInviteNames) { - invite.first_name = this.refs["first_name"+index].getDOMNode().value.trim(); - if (!invite.first_name && config.RequireInviteNames) { - first_name_errors[index] = "This is a required field"; + invite.firstName = this.refs['first_name' + index].getDOMNode().value.trim(); + if (!invite.firstName && config.RequireInviteNames) { + firstNameErrors[index] = 'This is a required field'; valid = false; } else { - first_name_errors[index] = ""; + firstNameErrors[index] = ''; } - invite.last_name = this.refs["last_name"+index].getDOMNode().value.trim(); - if (!invite.last_name && config.RequireInviteNames) { - last_name_errors[index] = "This is a required field"; + invite.lastName = this.refs['last_name' + index].getDOMNode().value.trim(); + if (!invite.lastName && config.RequireInviteNames) { + lastNameErrors[index] = 'This is a required field'; valid = false; } else { - last_name_errors[index] = ""; + lastNameErrors[index] = ''; } } invites.push(invite); } - this.setState({ email_errors: email_errors, first_name_errors: first_name_errors, last_name_errors: last_name_errors }); + this.setState({emailErrors: emailErrors, firstNameErrors: firstNameErrors, lastNameErrors: lastNameErrors}); - if (!valid || invites.length === 0) return; + if (!valid || invites.length === 0) { + return; + } - var data = {} - data["invites"] = invites; + var data = {}; + data.invites = invites; Client.inviteMembers(data, function() { @@ -89,146 +90,177 @@ module.exports = React.createClass({ $(this.refs.modal.getDOMNode()).modal('hide'); }.bind(this), function(err) { - if (err.message === "This person is already on your team") { - email_errors[err.detailed_error] = err.message; - this.setState({ email_errors: email_errors }); + if (err.message === 'This person is already on your team') { + emailErrors[err.detailed_error] = err.message; + this.setState({emailErrors: emailErrors}); + } else { + this.setState({serverError: err.message}); } - else - this.setState({ server_error: err.message}); }.bind(this) ); - }, componentDidUpdate: function() { $(this.refs.modalBody.getDOMNode()).css('max-height', $(window).height() - 200); $(this.refs.modalBody.getDOMNode()).css('overflow-y', 'scroll'); }, addInviteFields: function() { - var count = this.state.id_count + 1; - var invite_ids = this.state.invite_ids; - invite_ids.push(count); - this.setState({ invite_ids: invite_ids, id_count: count }); + var count = this.state.idCount + 1; + var inviteIds = this.state.inviteIds; + inviteIds.push(count); + this.setState({inviteIds: inviteIds, idCount: count}); }, clearFields: function() { - var invite_ids = this.state.invite_ids; + var inviteIds = this.state.inviteIds; - for (var i = 0; i < invite_ids.length; i++) { - var index = invite_ids[i]; - this.refs["email"+index].getDOMNode().value = ""; + for (var i = 0; i < inviteIds.length; i++) { + var index = inviteIds[i]; + this.refs['email' + index].getDOMNode().value = ''; if (config.AllowInviteNames) { - this.refs["first_name"+index].getDOMNode().value = ""; - this.refs["last_name"+index].getDOMNode().value = ""; + this.refs['first_name' + index].getDOMNode().value = ''; + this.refs['last_name' + index].getDOMNode().value = ''; } } this.setState({ - invite_ids: [0], - id_count: 0, - email_errors: {}, - first_name_errors: {}, - last_name_errors: {} + inviteIds: [0], + idCount: 0, + emailErrors: {}, + firstNameErrors: {}, + lastNameErrors: {} }); }, removeInviteFields: function(index) { - var count = this.state.id_count; - var invite_ids = this.state.invite_ids; - var i = invite_ids.indexOf(index); - if (i > -1) invite_ids.splice(i, 1); - if (!invite_ids.length) invite_ids.push(++count); - this.setState({ invite_ids: invite_ids, id_count: count }); + var count = this.state.idCount; + var inviteIds = this.state.inviteIds; + var i = inviteIds.indexOf(index); + if (i > -1) { + inviteIds.splice(i, 1); + } + if (!inviteIds.length) { + inviteIds.push(++count); + } + this.setState({inviteIds: inviteIds, idCount: count}); }, getInitialState: function() { return { - invite_ids: [0], - id_count: 0, - email_errors: {}, - first_name_errors: {}, - last_name_errors: {} + inviteIds: [0], + idCount: 0, + emailErrors: {}, + firstNameErrors: {}, + lastNameErrors: {} }; }, render: function() { - var currentUser = UserStore.getCurrentUser() + var currentUser = UserStore.getCurrentUser(); if (currentUser != null) { - var invite_sections = []; - var invite_ids = this.state.invite_ids; - var self = this; - for (var i = 0; i < invite_ids.length; i++) { - var index = invite_ids[i]; - var email_error = this.state.email_errors[index] ? <label className='control-label'>{ this.state.email_errors[index] }</label> : null; - var first_name_error = this.state.first_name_errors[index] ? <label className='control-label'>{ this.state.first_name_errors[index] }</label> : null; - var last_name_error = this.state.last_name_errors[index] ? <label className='control-label'>{ this.state.last_name_errors[index] }</label> : null; - - invite_sections[index] = ( - <div key={"key" + index}> - <div> - <button type="button" className="btn btn-link remove__member" onClick={this.removeInviteFields.bind(this, index)}><span className="fa fa-trash"></span></button> - </div> - <div className={ email_error ? "form-group invite has-error" : "form-group invite" }> - <input onKeyUp={this.displayNameKeyUp} type="text" ref={"email"+index} className="form-control" placeholder="email@domain.com" maxLength="64" /> - { email_error } - </div> - <div className="row--invite"> - { config.AllowInviteNames ? - <div className="col-sm-6"> - <div className={ first_name_error ? "form-group has-error" : "form-group" }> - <input type="text" className="form-control" ref={"first_name"+index} placeholder="First name" maxLength="64" /> - { first_name_error } - </div> - </div> - : "" } - { config.AllowInviteNames ? - <div className="col-sm-6"> - <div className={ last_name_error ? "form-group has-error" : "form-group" }> - <input type="text" className="form-control" ref={"last_name"+index} placeholder="Last name" maxLength="64" /> - { last_name_error } - </div> - </div> - : "" } + var inviteSections = []; + var inviteIds = this.state.inviteIds; + for (var i = 0; i < inviteIds.length; i++) { + var index = inviteIds[i]; + var emailError = null; + if (this.state.emailErrors[index]) { + emailError = <label className='control-label'>{this.state.emailErrors[index]}</label>; + } + var firstNameError = null; + if (this.state.firstNameErrors[index]) { + firstNameError = <label className='control-label'>{this.state.firstNameErrors[index]}</label>; + } + var lastNameError = null; + if (this.state.lastNameErrors[index]) { + lastNameError = <label className='control-label'>{this.state.lastNameErrors[index]}</label>; + } + + var removeButton = null; + if (index) { + removeButton = (<div> + <button type='button' className='btn btn-link remove__member' onClick={this.removeInviteFields.bind(this, index)}><span className='fa fa-trash'></span></button> + </div>); + } + var emailClass = 'form-group invite'; + if (emailError) { + emailClass += ' has-error'; + } + + var nameFields = null; + if (config.AllowInviteNames) { + var firstNameClass = 'form-group'; + if (firstNameError) { + firstNameClass += ' has-error'; + } + var lastNameClass = 'form-group'; + if (lastNameError) { + lastNameClass += ' has-error'; + } + nameFields = (<div className='row--invite'> + <div className='col-sm-6'> + <div className={firstNameClass}> + <input type='text' className='form-control' ref={'first_name' + index} placeholder='First name' maxLength='64' /> + {firstNameError} + </div> + </div> + <div className='col-sm-6'> + <div className={lastNameClass}> + <input type='text' className='form-control' ref={'last_name' + index} placeholder='Last name' maxLength='64' /> + {lastNameError} + </div> + </div> + </div>); + } + + inviteSections[index] = ( + <div key={'key' + index}> + {removeButton} + <div className={emailClass}> + <input onKeyUp={this.displayNameKeyUp} type='text' ref={'email' + index} className='form-control' placeholder='email@domain.com' maxLength='64' /> + {emailError} </div> + {nameFields} </div> ); } - 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 serverError = null; + if (this.state.serverError) { + serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>; + } return ( <div> - <div className="modal fade" ref="modal" id="invite_member" tabIndex="-1" role="dialog" aria-hidden="true"> - <div className="modal-dialog"> - <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">Invite New Member</h4> + <div className='modal fade' ref='modal' id='invite_member' tabIndex='-1' role='dialog' aria-hidden='true'> + <div className='modal-dialog'> + <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'>Invite New Member</h4> </div> - <div ref="modalBody" className="modal-body"> - <form role="form"> - { invite_sections } + <div ref='modalBody' className='modal-body'> + <form role='form'> + {inviteSections} </form> - { server_error } - <button type="button" className="btn btn-default" onClick={this.addInviteFields}>Add another</button> + {serverError} + <button type='button' className='btn btn-default' onClick={this.addInviteFields}>Add another</button> <br/> <br/> <span>People invited automatically join Town Square channel.</span> </div> - <div className="modal-footer"> - <button type="button" className="btn btn-default" data-dismiss="modal">Cancel</button> - <button onClick={this.handleSubmit} type="button" className="btn btn-primary">Send Invitations</button> + <div className='modal-footer'> + <button type='button' className='btn btn-default' data-dismiss='modal'>Cancel</button> + <button onClick={this.handleSubmit} type='button' className='btn btn-primary'>Send Invitations</button> </div> </div> </div> </div> <ConfirmModal - id="confirm_invite_modal" - parent_id="invite_member" - title="Discard Invitations?" - message="You have unsent invitations, are you sure you want to discard them?" - confirm_button="Yes, Discard" + id='confirm_invite_modal' + parent_id='invite_member' + title='Discard Invitations?' + message='You have unsent invitations, are you sure you want to discard them?' + confirm_button='Yes, Discard/' /> </div> ); - } else { - return <div/>; } + return <div/>; } }); diff --git a/web/react/components/mention_list.jsx b/web/react/components/mention_list.jsx index 71a6083d2..f562cfb29 100644 --- a/web/react/components/mention_list.jsx +++ b/web/react/components/mention_list.jsx @@ -15,81 +15,81 @@ var MAX_ITEMS_IN_LIST = 25; var ITEM_HEIGHT = 36; module.exports = React.createClass({ - displayName: "MentionList", + displayName: 'MentionList', componentDidMount: function() { - PostStore.addMentionDataChangeListener(this._onChange); + PostStore.addMentionDataChangeListener(this.onListenerChange); var self = this; - $('body').on('keydown.mentionlist', '#'+this.props.id, + $('.post-right__scroll').scroll(function(){ + if($('.mentions--top').length){ + $('#reply_mention_tab .mentions--top').css({ bottom: $(window).height() - $('.post-right__scroll #reply_textbox').offset().top }); + } + }); + + $('body').on('keydown.mentionlist', '#' + this.props.id, function(e) { - if (!self.isEmpty() && self.state.mentionText != '-1' && (e.which === 13 || e.which === 9)) { + if (!self.isEmpty() && self.state.mentionText !== '-1' && (e.which === 13 || e.which === 9)) { e.stopPropagation(); e.preventDefault(); self.addCurrentMention(); - } - else if (!self.isEmpty() && self.state.mentionText != '-1' && (e.which === 38 || e.which === 40)) { + } 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 }); + 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 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 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); + self.scrollToMention(e.which); } } ); $(document).click(function(e) { - if (!($('#'+self.props.id).is(e.target) || $('#'+self.props.id).has(e.target).length || - ('mentionlist' in self.refs && $(self.refs['mentionlist'].getDOMNode()).has(e.target).length))) { - self.setState({mentionText: "-1"}) + if (!($('#' + self.props.id).is(e.target) || $('#' + self.props.id).has(e.target).length || + ('mentionlist' in self.refs && $(self.refs.mentionlist.getDOMNode()).has(e.target).length))) { + self.setState({mentionText: '-1'}); } }); }, componentWillUnmount: function() { - PostStore.removeMentionDataChangeListener(this._onChange); - $('body').off('keydown.mentionlist', '#'+this.props.id); + PostStore.removeMentionDataChangeListener(this.onListenerChange); + $('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)) { + 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 }); + this.setState({selectedMention: tempSelectedMention}); foundMatch = true; break; } } if (this.getSelection(0) && !foundMatch) { - this.setState({ selectedMention: 0, selectedUsername: this.refs.mention0.props.username }); + this.setState({selectedMention: 0, selectedUsername: this.refs.mention0.props.username}); } } - } - else if (this.state.selectedMention !== 0) { - this.setState({ selectedMention: 0, selectedUsername: "" }); + } else if (this.state.selectedMention !== 0) { + this.setState({selectedMention: 0, selectedUsername: ''}); } }, - _onChange: function(id, mentionText, excludeList) { - if (id !== this.props.id) return; + onListenerChange: function(id, mentionText) { + if (id !== this.props.id) { + return; + } var newState = this.state; - if (mentionText != null) newState.mentionText = mentionText; - if (excludeList != null) newState.excludeUsers = excludeList; + if (mentionText != null) { + newState.mentionText = mentionText; + } this.setState(newState); }, @@ -100,63 +100,61 @@ module.exports = React.createClass({ username: name }); - this.setState({ mentionText: '-1' }); + this.setState({mentionText: '-1'}); }, handleMouseEnter: function(listId) { - this.setState({ selectedMention: listId, selectedUsername: this.refs['mention' + listId].props.username }); + this.setState({selectedMention: listId, selectedUsername: this.refs['mention' + listId].props.username}); }, getSelection: function(listId) { - if (!this.refs['mention' + listId]) + if (!this.refs['mention' + listId]) { return false; - else - return true; + } + return true; }, addCurrentMention: function() { - if (!this.getSelection(this.state.selectedMention)) + if (!this.getSelection(this.state.selectedMention)) { this.addFirstMention(); - else + } else { this.refs['mention' + this.state.selectedMention].handleClick(); + } }, addFirstMention: function() { - if (!this.refs.mention0) return; + if (!this.refs.mention0) { + return; + } this.refs.mention0.handleClick(); }, isEmpty: function() { return (!this.refs.mention0); }, - scrollToMention: function(keyPressed, ifLoopUp) { - var direction = keyPressed === 38 ? "up" : "down"; + scrollToMention: function(keyPressed) { + var direction; + if (keyPressed === 38) { + direction = 'up'; + } else { + direction = '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); + 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({ + $('#mentionsbox').animate({ scrollTop: scrollAmount }, 75); }, - alreadyMentioned: function(username) { - var excludeUsers = this.state.excludeUsers; - for (var i = 0; i < excludeUsers.length; i++) { - if (excludeUsers[i] === username) { - return true; - } - } - return false; - }, getInitialState: function() { - return { excludeUsers: [], mentionText: "-1", selectedMention: 0, selectedUsername: "" }; + return {excludeUsers: [], mentionText: '-1', selectedMention: 0, selectedUsername: ''}; }, render: function() { var self = this; var mentionText = this.state.mentionText; - if (mentionText === '-1') return null; + if (mentionText === '-1') { + return null; + } var profiles = UserStore.getActiveOnlyProfiles(); var users = []; @@ -165,32 +163,35 @@ module.exports = React.createClass({ } var all = {}; - all.username = "all"; - all.nickname = ""; - all.secondary_text = "Notifies everyone in the team"; - all.id = "allmention"; + all.username = 'all'; + all.nickname = ''; + all.secondary_text = 'Notifies everyone in the team'; + all.id = 'allmention'; users.push(all); var channel = {}; - channel.username = "channel"; - channel.nickname = ""; - channel.secondary_text = "Notifies everyone in the channel"; - channel.id = "channelmention"; + channel.username = 'channel'; + channel.nickname = ''; + channel.secondary_text = 'Notifies everyone in the channel'; + channel.id = 'channelmention'; users.push(channel); - users.sort(function(a,b) { - if (a.username < b.username) return -1; - if (a.username > b.username) return 1; + users.sort(function(a, b) { + if (a.username < b.username) { + return -1; + } + if (a.username > b.username) { + return 1; + } return 0; }); var mentions = {}; var index = 0; for (var i = 0; i < users.length && index < MAX_ITEMS_IN_LIST; i++) { - if (this.alreadyMentioned(users[i].username)) continue; - - 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) { + 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} @@ -198,7 +199,7 @@ module.exports = React.createClass({ secondary_text={Utils.getFullName(users[i])} id={users[i].id} listId={index} - isFocused={this.state.selectedMention === index ? "mentions-focus" : ""} + isFocused={this.state.selectedMention === index ? 'mentions-focus' : ''} handleMouseEnter={function(value) { return function() { self.handleMouseEnter(value); } }(index)} handleClick={this.handleClick} /> ); @@ -208,21 +209,23 @@ module.exports = React.createClass({ var numMentions = Object.keys(mentions).length; - if (numMentions < 1) return null; + if (numMentions < 1) { + return null; + } - var $mention_tab = $('#'+this.props.id); - var maxHeight = Math.min(MAX_HEIGHT_LIST, $mention_tab.offset().top - 10); + var $mentionTab = $('#' + this.props.id); + var maxHeight = Math.min(MAX_HEIGHT_LIST, $mentionTab.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 + height: Math.min(maxHeight, (numMentions * ITEM_HEIGHT) + 4), + width: $mentionTab.parent().width(), + bottom: $(window).height() - $mentionTab.offset().top, + left: $mentionTab.offset().left }; return ( - <div className="mentions--top" style={style}> - <div ref="mentionlist" className="mentions-box" id="mentionsbox"> - { mentions } + <div className='mentions--top' style={style}> + <div ref='mentionlist' className='mentions-box' id='mentionsbox'> + {mentions} </div> </div> ); diff --git a/web/react/components/new_channel.jsx b/web/react/components/new_channel.jsx index 49e088458..c22147022 100644 --- a/web/react/components/new_channel.jsx +++ b/web/react/components/new_channel.jsx @@ -1,138 +1,151 @@ // 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 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({ + displayName: 'NewChannelModal', handleSubmit: function(e) { e.preventDefault(); var channel = {}; - var state = { server_error: "" }; + var state = {serverError: ''}; channel.display_name = this.refs.display_name.getDOMNode().value.trim(); if (!channel.display_name) { - state.display_name_error = "This field is required"; + state.displayNameError = 'This field is required'; state.inValid = true; - } - else if (channel.display_name.length > 22) { - state.display_name_error = "This field must be less than 22 characters"; + } else if (channel.display_name.length > 22) { + state.displayNameError = 'This field must be less than 22 characters'; state.inValid = true; - } - else { - state.display_name_error = ""; + } else { + state.displayNameError = ''; } channel.name = this.refs.channel_name.getDOMNode().value.trim(); if (!channel.name) { - state.name_error = "This field is required"; + state.nameError = 'This field is required'; state.inValid = true; - } - else if(channel.name.length > 22){ - state.name_error = "This field must be less than 22 characters"; + } else if (channel.name.length > 22) { + state.nameError = 'This field must be less than 22 characters'; state.inValid = true; - } - else { - var cleaned_name = utils.cleanUpUrlable(channel.name); - if (cleaned_name != channel.name) { - state.name_error = "Must be lowercase alphanumeric characters, allowing '-' but not starting or ending with '-'"; + } else { + var cleanedName = utils.cleanUpUrlable(channel.name); + if (cleanedName !== channel.name) { + state.nameError = "Must be lowercase alphanumeric characters, allowing '-' but not starting or ending with '-'"; state.inValid = true; - } - else { - state.name_error = ""; + } else { + state.nameError = ''; } } this.setState(state); - if (state.inValid) + if (state.inValid) { return; + } var cu = UserStore.getCurrentUser(); channel.team_id = cu.team_id; channel.description = this.refs.channel_desc.getDOMNode().value.trim(); - channel.type = this.state.channel_type; + channel.type = this.state.channelType; var self = this; client.createChannel(channel, function() { - this.refs.display_name.getDOMNode().value = ""; - this.refs.channel_name.getDOMNode().value = ""; - this.refs.channel_desc.getDOMNode().value = ""; + 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 = TeamStore.getCurrentTeamUrl() + "/channels/" + channel.name; + window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name; asyncClient.getChannels(true); }.bind(this), function(err) { - state.server_error = err.message; + state.serverError = err.message; state.inValid = true; this.setState(state); }.bind(this) ); }, - displayNameKeyUp: function(e) { - var display_name = this.refs.display_name.getDOMNode().value.trim(); - var channel_name = utils.cleanUpUrlable(display_name); - this.refs.channel_name.getDOMNode().value = channel_name; + displayNameKeyUp: function() { + var displayName = this.refs.display_name.getDOMNode().value.trim(); + var channelName = utils.cleanUpUrlable(displayName); + this.refs.channel_name.getDOMNode().value = channelName; }, componentDidMount: function() { var self = this; $(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) { var button = e.relatedTarget; - self.setState({ channel_type: $(button).attr('data-channeltype') }); + self.setState({channelType: $(button).attr('data-channeltype')}); }); }, getInitialState: function() { - return { channel_type: "" }; + return {channelType: ''}; }, render: function() { + var displayNameError = null; + var nameError = null; + var serverError = null; + var displayNameClass = 'form-group'; + var nameClass = 'form-group'; - var display_name_error = this.state.display_name_error ? <label className='control-label'>{ this.state.display_name_error }</label> : null; - var name_error = this.state.name_error ? <label className='control-label'>{ this.state.name_error }</label> : null; - var server_error = this.state.server_error ? <div className='form-group has-error'><label className='control-label'>{ this.state.server_error }</label></div> : null; + if (this.state.displayNameError) { + displayNameError = <label className='control-label'>{this.state.displayNameError}</label>; + displayNameClass += ' has-error'; + } + if (this.state.nameError) { + nameError = <label className='control-label'>{this.state.nameError}</label>; + nameClass += ' has-error'; + } + if (this.state.serverError) { + serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>; + } + + var channelTerm = 'Channel'; + if (this.state.channelType === 'P') { + channelTerm = 'Group'; + } return ( - <div className="modal fade" id="new_channel" ref="modal" tabIndex="-1" role="dialog" aria-hidden="true"> - <div className="modal-dialog"> - <div className="modal-content"> - <div className="modal-header"> - <button type="button" className="close" data-dismiss="modal"> - <span aria-hidden="true">×</span> - <span className="sr-only">Close</span> + <div className='modal fade' id='new_channel' ref='modal' tabIndex='-1' role='dialog' aria-hidden='true'> + <div className='modal-dialog'> + <div className='modal-content'> + <div className='modal-header'> + <button type='button' className='close' data-dismiss='modal'> + <span aria-hidden='true'>×</span> + <span className='sr-only'>Cancel</span> </button> - <h4 className="modal-title">New Channel</h4> + <h4 className='modal-title'>New {channelTerm}</h4> </div> - <div className="modal-body"> - <form role="form"> - <div className={ this.state.display_name_error ? "form-group has-error" : "form-group" }> + <form role='form'> + <div className='modal-body'> + <div className={displayNameClass}> <label className='control-label'>Display Name</label> - <input onKeyUp={this.displayNameKeyUp} type="text" ref="display_name" className="form-control" placeholder="Enter display name" maxLength="64" /> - { display_name_error } + <input onKeyUp={this.displayNameKeyUp} type='text' ref='display_name' className='form-control' placeholder='Enter display name' maxLength='64' /> + {displayNameError} </div> - <div className={ this.state.name_error ? "form-group has-error" : "form-group" }> + <div className={nameClass}> <label className='control-label'>Handle</label> - <input type="text" className="form-control" ref="channel_name" placeholder="lowercase alphanumeric's only" maxLength="64" /> - { name_error } + <input type='text' className='form-control' ref='channel_name' placeholder="lowercase alphanumeric's only" maxLength='64' /> + {nameError} </div> - <div className="form-group"> + <div className='form-group'> <label className='control-label'>Description</label> - <textarea className="form-control no-resize" 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> - </div> - <div className="modal-footer"> - <button type="button" className="btn btn-default" data-dismiss="modal">Close</button> - <button onClick={this.handleSubmit} type="button" className="btn btn-primary">Create New Channel</button> - </div> + {serverError} + </div> + <div className='modal-footer'> + <button type='button' className='btn btn-default' data-dismiss='modal'>Cancel</button> + <button onClick={this.handleSubmit} type='submit' className='btn btn-primary'>Create New {channelTerm}</button> + </div> + </form> </div> </div> </div> diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index 3f59d5843..bb1b1704c 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -37,15 +37,23 @@ module.exports = React.createClass({ componentDidMount: function() { var user = UserStore.getCurrentUser(); if (user.props && user.props.theme) { - 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: ' + 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+';'); utils.changeCss('@media(max-width: 768px){.search-bar__container', 'background: ' + user.props.theme+';}'); } + if (user.props.theme != '#000000' && user.props.theme != '#585858') { + utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, -10) +';'); + utils.changeCss('a.theme', 'color:'+user.props.theme+'; fill:'+user.props.theme+'!important;'); + } else if (user.props.theme == '#000000') { + utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, +50) +';'); + $('.team__header').addClass('theme--black'); + } else if (user.props.theme == '#585858') { + utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, +10) +';'); + $('.team__header').addClass('theme--gray'); + } PostStore.addChangeListener(this._onChange); ChannelStore.addChangeListener(this._onChange); @@ -311,7 +319,6 @@ module.exports = React.createClass({ } else if (channel.type === 'D') { var teammate = utils.getDirectTeammate(channel.id) - if (teammate) { var teammate_name = teammate.nickname.length > 0 ? teammate.nickname : teammate.username; more_messages = ( @@ -399,7 +406,7 @@ module.exports = React.createClass({ var postCtls = []; if (posts) { - var previousPostDay = posts[order[order.length-1]] ? utils.getDateForUnixTicks(posts[order[order.length-1]].create_at): new Date(); + var previousPostDay = new Date(0); var currentPostDay; for (var i = order.length-1; i >= 0; i--) { diff --git a/web/react/components/setting_picture.jsx b/web/react/components/setting_picture.jsx index fa4c8bb62..e97b67706 100644 --- a/web/react/components/setting_picture.jsx +++ b/web/react/components/setting_picture.jsx @@ -20,8 +20,14 @@ module.exports = React.createClass({ } }, render: function() { - var client_error = this.props.client_error ? <div className='form-group has-error'><label className='control-label'>{ this.props.client_error }</label></div> : null; - var server_error = this.props.server_error ? <div className='form-group has-error'><label className='control-label'>{ this.props.server_error }</label></div> : null; + var clientError = null; + if (this.props.client_error) { + clientError = <div className='form-group has-error'><label className='control-label'>{this.props.client_error}</label></div>; + } + var serverError = null; + if (this.props.server_error) { + serverError = <div className='form-group has-error'><label className='control-label'>{this.props.server_error}</label></div>; + } var img = null; if (this.props.picture) { @@ -30,8 +36,20 @@ module.exports = React.createClass({ img = (<img ref='image' className='profile-img' src={this.props.src}/>); } - var self = this; + var confirmButton; + if (this.props.loadingPicture) { + confirmButton = <img className='spinner' src='/static/images/load.gif'/>; + } else { + var confirmButtonClass = 'btn btn-sm'; + if (this.props.submitActive) { + confirmButtonClass += ' btn-primary'; + } else { + confirmButtonClass += ' btn-inactive disabled'; + } + confirmButton = <a className={confirmButtonClass} onClick={this.props.submit}>Save</a>; + } + var self = this; return ( <ul className='section-max'> <li className='col-xs-12 section-title'>{this.props.title}</li> @@ -41,10 +59,10 @@ module.exports = React.createClass({ {img} </li> <li className='setting-list-item'> - {server_error} - {client_error} + {serverError} + {clientError} <span className='btn btn-sm btn-primary btn-file sel-btn'>Select<input ref='input' accept='.jpg,.png,.bmp' type='file' onChange={this.props.pictureChange}/></span> - <a className={this.props.submitActive ? 'btn btn-sm btn-primary' : 'btn btn-sm btn-inactive disabled'} onClick={this.props.submit}>Save</a> + {confirmButton} <a className='btn btn-sm theme' href='#' onClick={self.props.updateSection}>Cancel</a> </li> </ul> diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 5b8d6c542..1d39f5f67 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -7,7 +7,7 @@ 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 BrowserStore = require('../stores/browser_store.jsx') +var BrowserStore = require('../stores/browser_store.jsx'); var utils = require('../utils/utils.jsx'); var SidebarHeader = require('./sidebar_header.jsx'); var SearchBox = require('./search_bar.jsx'); @@ -17,13 +17,15 @@ var ActionTypes = Constants.ActionTypes; function getStateFromStores() { var members = ChannelStore.getAllMembers(); - var team_member_map = UserStore.getActiveOnlyProfiles(); - var current_id = ChannelStore.getCurrentId(); + var teamMemberMap = UserStore.getActiveOnlyProfiles(); + var currentId = ChannelStore.getCurrentId(); var teammates = []; - for (var id in team_member_map) { - if (id === UserStore.getCurrentId()) continue; - teammates.push(team_member_map[id]); + for (var id in teamMemberMap) { + if (id === UserStore.getCurrentId()) { + continue; + } + teammates.push(teamMemberMap[id]); } // Create lists of all read and unread direct channels @@ -32,11 +34,11 @@ function getStateFromStores() { for (var i = 0; i < teammates.length; i++) { var teammate = teammates[i]; - if (teammate.id == UserStore.getCurrentId()) { + if (teammate.id === UserStore.getCurrentId()) { continue; } - var channelName = ""; + var channelName = ''; if (teammate.id > UserStore.getCurrentId()) { channelName = UserStore.getCurrentId() + '__' + teammate.id; } else { @@ -46,17 +48,17 @@ function getStateFromStores() { var channel = ChannelStore.getByName(channelName); if (channel != null) { - channel.display_name = utils.getDisplayName(teammate); + channel.display_name = teammate.username; channel.teammate_username = teammate.username; channel.status = UserStore.getStatus(teammate.id); var channelMember = members[channel.id]; - var msg_count = channel.total_msg_count - channelMember.msg_count; - if (msg_count > 0) { - channel.unread = msg_count; + var msgCount = channel.total_msg_count - channelMember.msg_count; + if (msgCount > 0) { + channel.unread = msgCount; showDirectChannels.push(channel); - } else if (current_id === channel.id) { + } else if (currentId === channel.id) { showDirectChannels.push(channel); } else { readDirectChannels.push(channel); @@ -74,13 +76,22 @@ function getStateFromStores() { // If we don't have MAX_DMS unread channels, sort the read list by last_post_at if (showDirectChannels.length < Constants.MAX_DMS) { - readDirectChannels.sort(function(a,b) { + readDirectChannels.sort(function(a, b) { // sort by last_post_at first - if (a.last_post_at > b.last_post_at) return -1; - if (a.last_post_at < b.last_post_at) return 1; + if (a.last_post_at > b.last_post_at) { + return -1; + } + if (a.last_post_at < b.last_post_at) { + return 1; + } + // if last_post_at is equal, sort by name - if (a.display_name < b.display_name) return -1; - if (a.display_name > b.display_name) return 1; + if (a.display_name < b.display_name) { + return -1; + } + if (a.display_name > b.display_name) { + return 1; + } return 0; }); @@ -91,15 +102,19 @@ function getStateFromStores() { } readDirectChannels = readDirectChannels.slice(index); - showDirectChannels.sort(function(a,b) { - if (a.display_name < b.display_name) return -1; - if (a.display_name > b.display_name) return 1; + showDirectChannels.sort(function(a, b) { + if (a.display_name < b.display_name) { + return -1; + } + if (a.display_name > b.display_name) { + return 1; + } return 0; }); } return { - active_id: current_id, + active_id: currentId, channels: ChannelStore.getAll(), members: members, showDirectChannels: showDirectChannels, @@ -108,12 +123,13 @@ function getStateFromStores() { } module.exports = React.createClass({ + displayName: 'Sidebar', componentDidMount: function() { - ChannelStore.addChangeListener(this._onChange); - UserStore.addChangeListener(this._onChange); - UserStore.addStatusesChangeListener(this._onChange); - SocketStore.addChangeListener(this._onSocketChange); - $(".nav-pills__container").perfectScrollbar(); + ChannelStore.addChangeListener(this.onChange); + UserStore.addChangeListener(this.onChange); + UserStore.addStatusesChangeListener(this.onChange); + SocketStore.addChangeListener(this.onSocketChange); + $('.nav-pills__container').perfectScrollbar(); this.updateTitle(); }, @@ -121,93 +137,88 @@ module.exports = React.createClass({ this.updateTitle(); }, componentWillUnmount: function() { - ChannelStore.removeChangeListener(this._onChange); - UserStore.removeChangeListener(this._onChange); - UserStore.removeStatusesChangeListener(this._onChange); - SocketStore.removeChangeListener(this._onSocketChange); + ChannelStore.removeChangeListener(this.onChange); + UserStore.removeChangeListener(this.onChange); + UserStore.removeStatusesChangeListener(this.onChange); + SocketStore.removeChangeListener(this.onSocketChange); }, - _onChange: function() { + onChange: function() { var newState = getStateFromStores(); if (!utils.areStatesEqual(newState, this.state)) { this.setState(newState); } }, - _onSocketChange: function(msg) { - if (msg.action == "posted") { + onSocketChange: function(msg) { + if (msg.action === 'posted') { if (ChannelStore.getCurrentId() === msg.channel_id) { AsyncClient.getChannels(true, window.isActive); } else { AsyncClient.getChannels(true); } - if (UserStore.getCurrentId() != msg.user_id) { - + if (UserStore.getCurrentId() !== msg.user_id) { var mentions = msg.props.mentions ? JSON.parse(msg.props.mentions) : []; var channel = ChannelStore.get(msg.channel_id); var user = UserStore.getCurrentUser(); - if (user.notify_props && ((user.notify_props.desktop === "mention" && mentions.indexOf(user.id) === -1 && channel.type !== 'D') || user.notify_props.desktop === "none")) { + if (user.notify_props && ((user.notify_props.desktop === 'mention' && mentions.indexOf(user.id) === -1 && channel.type !== 'D') || user.notify_props.desktop === 'none')) { return; } var member = ChannelStore.getMember(msg.channel_id); - if ((member.notify_level === "mention" && mentions.indexOf(user.id) === -1) || member.notify_level === "none" || member.notify_level === "quiet") { + if ((member.notify_level === 'mention' && mentions.indexOf(user.id) === -1) || member.notify_level === 'none' || member.notify_level === 'quiet') { return; } - var username = "Someone"; + var username = 'Someone'; if (UserStore.hasProfile(msg.user_id)) { username = UserStore.getProfile(msg.user_id).username; } - var title = channel ? channel.display_name : "Posted"; + var title = channel ? channel.display_name : 'Posted'; - var repRegex = new RegExp("<br>", "g"); + var repRegex = new RegExp('<br>', 'g'); var post = JSON.parse(msg.props.post); 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) + "..."; + var notifyText = post.message.replace(repRegex, '\n').replace(/\n+/g, ' ').replace('<mention>', '').replace('</mention>', ''); + + if (notifyText.length > 50) { + notifyText = notifyText.substring(0, 49) + '...'; } - if (msg.length === 0) { + if (notifyText.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); + 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 + " did something new", channel); - } - } - else { - utils.notifyMe(title, username + " wrote: " + msg, channel); + } else { + utils.notifyMe(title, username + ' wrote: ' + notifyText, channel); } - if (!user.notify_props || user.notify_props.desktop_sound === "true") { + if (!user.notify_props || user.notify_props.desktop_sound === 'true') { utils.ding(); } } - - } else if (msg.action == "viewed") { + } else if (msg.action === 'viewed') { if (ChannelStore.getCurrentId() != msg.channel_id) { AsyncClient.getChannels(true); } - } else if (msg.action == "user_added") { + } else if (msg.action === 'user_added') { if (UserStore.getCurrentId() === msg.user_id) { AsyncClient.getChannels(true); } - } else if(msg.action === "user_removed") { - if(msg.user_id === UserStore.getCurrentId()) { + } else if (msg.action === 'user_removed') { + if (msg.user_id === UserStore.getCurrentId()) { AsyncClient.getChannels(true); - if(msg.props.channel_id === ChannelStore.getCurrentId() && $('#removed_from_channel').length > 0) { + if (msg.props.channel_id === ChannelStore.getCurrentId() && $('#removed_from_channel').length > 0) { var sentState = {}; sentState.channelName = ChannelStore.getCurrent().display_name; sentState.remover = UserStore.getProfile(msg.props.remover).username; - BrowserStore.setItem('channel-removed-state',sentState); + BrowserStore.setItem('channel-removed-state', sentState); $('#removed_from_channel').modal('show'); } } @@ -217,10 +228,10 @@ module.exports = React.createClass({ var channel = ChannelStore.getCurrent(); if (channel) { if (channel.type === 'D') { - var teammate_username = utils.getDirectTeammate(channel.id).username - document.title = teammate_username + " " + document.title.substring(document.title.lastIndexOf("-")); + var teammate_username = utils.getDirectTeammate(channel.id).username; + document.title = teammate_username + ' ' + document.title.substring(document.title.lastIndexOf('-')); } else { - document.title = channel.display_name + " " + document.title.substring(document.title.lastIndexOf("-")) + document.title = channel.display_name + ' ' + document.title.substring(document.title.lastIndexOf('-')); } } }, @@ -229,92 +240,96 @@ module.exports = React.createClass({ }, render: function() { var members = this.state.members; - var newsActive = window.location.pathname === "/" ? "active" : ""; + var newsActive = window.location.pathname === '/' ? 'active' : ''; var badgesActive = false; var self = this; var channelItems = this.state.channels.map(function(channel) { if (channel.type != 'O') { - return ""; + return ''; } var channelMember = members[channel.id]; - var active = channel.id === self.state.active_id ? "active" : ""; + var active = channel.id === self.state.active_id ? 'active' : ''; - var msg_count = channel.total_msg_count - channelMember.msg_count; - var titleClass = "" - if (msg_count > 0 && channelMember.notify_level !== "quiet") { - titleClass = "unread-title" + var msgCount = channel.total_msg_count - channelMember.msg_count; + var titleClass = ''; + if (msgCount > 0 && channelMember.notify_level !== 'quiet') { + titleClass = 'unread-title'; } - var badge = ""; + var badge = ''; if (channelMember.mention_count > 0) { - badge = <span className="badge pull-right small">{channelMember.mention_count}</span>; + badge = <span className='badge pull-right small'>{channelMember.mention_count}</span>; badgesActive = true; - titleClass = "unread-title" + titleClass = 'unread-title'; } return ( - <li key={channel.id} className={active}><a className={"sidebar-channel " + titleClass} href="#" onClick={function(e){e.preventDefault(); utils.switchChannel(channel);}}>{badge}{channel.display_name}</a></li> + <li key={channel.id} className={active}><a className={'sidebar-channel ' + titleClass} href='#' onClick={function(e){e.preventDefault(); utils.switchChannel(channel);}}>{badge}{channel.display_name}</a></li> ); }); var privateChannelItems = this.state.channels.map(function(channel) { - if (channel.type != 'P') { - return ""; + if (channel.type !== 'P') { + return ''; } var channelMember = members[channel.id]; - var active = channel.id === self.state.active_id ? "active" : ""; + var active = channel.id === self.state.active_id ? 'active' : ''; - var msg_count = channel.total_msg_count - channelMember.msg_count; - var titleClass = "" - if (msg_count > 0 && channelMember.notify_level !== "quiet") { - titleClass = "unread-title" + var msgCount = channel.total_msg_count - channelMember.msg_count; + var titleClass = '' + if (msgCount > 0 && channelMember.notify_level !== 'quiet') { + titleClass = 'unread-title' } - var badge = ""; + var badge = ''; if (channelMember.mention_count > 0) { - badge = <span className="badge pull-right small">{channelMember.mention_count}</span>; + badge = <span className='badge pull-right small'>{channelMember.mention_count}</span>; badgesActive = true; - titleClass = "unread-title" + titleClass = 'unread-title'; } return ( - <li key={channel.id} className={active}><a className={"sidebar-channel " + titleClass} href="#" onClick={function(e){e.preventDefault(); utils.switchChannel(channel);}}>{badge}{channel.display_name}</a></li> + <li key={channel.id} className={active}><a className={'sidebar-channel ' + titleClass} href='#' onClick={function(e){e.preventDefault(); utils.switchChannel(channel);}}>{badge}{channel.display_name}</a></li> ); }); var directMessageItems = this.state.showDirectChannels.map(function(channel) { - var badge = ""; - var titleClass = ""; + var badge = ''; + var titleClass = ''; - var statusIcon = ""; - if (channel.status === "online") { + var statusIcon = ''; + if (channel.status === 'online') { statusIcon = Constants.ONLINE_ICON_SVG; - } else if (channel.status === "away") { + } else if (channel.status === 'away') { statusIcon = Constants.ONLINE_ICON_SVG; } else { statusIcon = Constants.OFFLINE_ICON_SVG; } if (!channel.fake) { - var active = channel.id === self.state.active_id ? "active" : ""; + var active = channel.id === self.state.active_id ? 'active' : ''; if (channel.unread) { - badge = <span className="badge pull-right small">{channel.unread}</span>; + badge = <span className='badge pull-right small'>{channel.unread}</span>; badgesActive = true; - titleClass = "unread-title" + titleClass = 'unread-title'; + } + + function handleClick(e) { + e.preventDefault(); + utils.switchChannel(channel, channel.teammate_username); } return ( - <li key={channel.name} className={active}><a className={"sidebar-channel " + titleClass} href="#" onClick={function(e){e.preventDefault(); utils.switchChannel(channel, channel.teammate_username);}}><span className="status" dangerouslySetInnerHTML={{__html: statusIcon}} /> {badge}{channel.display_name}</a></li> + <li key={channel.name} className={active}><a className={'sidebar-channel ' + titleClass} href='#' onClick={handleClick}><span className='status' dangerouslySetInnerHTML={{__html: statusIcon}} /> {badge}{channel.display_name}</a></li> ); } else { return ( - <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> + <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> ); } - }); var link = document.createElement('link'); @@ -345,23 +360,23 @@ module.exports = React.createClass({ <SidebarHeader teamDisplayName={this.props.teamDisplayName} teamType={this.props.teamType} /> <SearchBox /> - <div className="nav-pills__container"> - <ul className="nav nav-pills nav-stacked"> - <li><h4>Channels<a className="add-channel-btn" href="#" data-toggle="modal" data-target="#new_channel" data-channeltype="O">+</a></h4></li> + <div className='nav-pills__container'> + <ul className='nav nav-pills nav-stacked'> + <li><h4>Channels<a className='add-channel-btn' href='#' data-toggle='modal' data-target='#new_channel' data-channeltype='O'>+</a></h4></li> {channelItems} - <li><a href="#" data-toggle="modal" className="nav-more" data-target="#more_channels" data-channeltype="O">More...</a></li> + <li><a href='#' data-toggle='modal' className='nav-more' data-target='#more_channels' data-channeltype='O'>More...</a></li> </ul> - <ul className="nav nav-pills nav-stacked"> - <li><h4>Private Groups<a className="add-channel-btn" href="#" data-toggle="modal" data-target="#new_channel" data-channeltype="P">+</a></h4></li> + <ul className='nav nav-pills nav-stacked'> + <li><h4>Private Groups<a className='add-channel-btn' href='#' data-toggle='modal' data-target='#new_channel' data-channeltype='P'>+</a></h4></li> {privateChannelItems} </ul> - <ul className="nav nav-pills nav-stacked"> + <ul className='nav nav-pills nav-stacked'> <li><h4>Private Messages</h4></li> {directMessageItems} { this.state.hideDirectChannels.length > 0 ? - <li><a href="#" data-toggle="modal" className="nav-more" data-target="#more_direct_channels" data-channels={JSON.stringify(this.state.hideDirectChannels)}>{"More ("+this.state.hideDirectChannels.length+")"}</a></li> - : "" } + <li><a href='#' data-toggle='modal' className='nav-more' data-target='#more_direct_channels' data-channels={JSON.stringify(this.state.hideDirectChannels)}>{'More ('+this.state.hideDirectChannels.length+')'}</a></li> + : '' } </ul> </div> </div> diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx index e01ddcd05..72b8547e5 100644 --- a/web/react/components/sidebar_header.jsx +++ b/web/react/components/sidebar_header.jsx @@ -1,15 +1,15 @@ // 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 Constants = require('../utils/constants.jsx'); function getStateFromStores() { - return { teams: UserStore.getTeams() }; + return {teams: UserStore.getTeams(), currentTeam: TeamStore.getCurrent()}; } var NavbarDropdown = React.createClass({ @@ -19,20 +19,24 @@ var NavbarDropdown = React.createClass({ }, blockToggle: false, componentDidMount: function() { - UserStore.addTeamsChangeListener(this._onChange); + UserStore.addTeamsChangeListener(this.onListenerChange); + TeamStore.addChangeListener(this.onListenerChange); var self = this; - $(this.refs.dropdown.getDOMNode()).on('hide.bs.dropdown', function(e) { + $(this.refs.dropdown.getDOMNode()).on('hide.bs.dropdown', function() { self.blockToggle = true; - setTimeout(function(){self.blockToggle = false;}, 100); + setTimeout(function() { + self.blockToggle = false; + }, 100); }); }, componentWillUnmount: function() { - UserStore.removeTeamsChangeListener(this._onChange); + UserStore.removeTeamsChangeListener(this.onListenerChange); + TeamStore.removeChangeListener(this.onListenerChange); $(this.refs.dropdown.getDOMNode()).off('hide.bs.dropdown'); }, - _onChange: function() { + onListenerChange: function() { if (this.isMounted()) { var newState = getStateFromStores(); if (!utils.areStatesEqual(newState, this.state)) { @@ -44,62 +48,65 @@ var NavbarDropdown = React.createClass({ return getStateFromStores(); }, render: function() { - var team_link = ""; - var invite_link = ""; - var manage_link = ""; - var rename_link = ""; + var teamLink = ''; + var inviteLink = ''; + var manageLink = ''; + var renameLink = ''; var currentUser = UserStore.getCurrentUser(); var isAdmin = false; + var teamSettings = null; if (currentUser != null) { - isAdmin = currentUser.roles.indexOf("admin") > -1; + isAdmin = currentUser.roles.indexOf('admin') > -1; - invite_link = ( <li> <a href="#" data-toggle="modal" data-target="#invite_member">Invite New Member</a> </li>); + inviteLink = (<li> <a href='#' data-toggle='modal' data-target='#invite_member'>Invite New Member</a> </li>); - if (this.props.teamType == "O") { - team_link = ( + if (this.props.teamType === 'O') { + teamLink = ( <li> - <a href="#" data-toggle="modal" data-target="#get_link" data-title="Team Invite" data-value={location.origin+"/signup_user_complete/?id="+currentUser.team_id}>Get Team Invite Link</a> + <a href='#' data-toggle='modal' data-target='#get_link' data-title='Team Invite' data-value={utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + currentUser.team_id}>Get Team Invite Link</a> </li> ); } } 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>); + manageLink = (<li> <a href='#' data-toggle='modal' data-target='#team_members'>Manage Team</a> </li>); + renameLink = (<li> <a href='#' data-toggle='modal' data-target='#rename_team_link'>Rename</a> </li>); + teamSettings = (<li> <a href='#' data-toggle='modal' data-target='#team_settings'>Team Settings</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 teamName = this.state.teams[i]; - - teams.push(<li key={ teamName }><a href={utils.getWindowLocationOrigin() + "/" + teamName }>Switch to { teamName }</a></li>); - } + teams.push(<li className='divider' key='div'></li>); + if (this.state.teams.length > 1 && this.state.currentTeam) { + var curTeamName = this.state.currentTeam.name; + this.state.teams.forEach(function(teamName) { + if (teamName !== curTeamName) { + teams.push(<li key={teamName}><a href={utils.getWindowLocationOrigin() + '/' + teamName}>Switch to {teamName}</a></li>); + } + }); } - teams.push(<li><a href={utils.getWindowLocationOrigin() + "/signup_team" }>Create a New Team</a></li>); + teams.push(<li><a href={utils.getWindowLocationOrigin() + '/signup_team'}>Create a New Team</a></li>); return ( - <ul className="nav navbar-nav navbar-right"> - <li ref="dropdown" className="dropdown"> - <a href="#" className="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"> - <span className="dropdown__icon" dangerouslySetInnerHTML={{__html: Constants.MENU_ICON }} /> + <ul className='nav navbar-nav navbar-right'> + <li ref='dropdown' className='dropdown'> + <a href='#' className='dropdown-toggle' data-toggle='dropdown' role='button' aria-expanded='false'> + <span className='dropdown__icon' dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} /> </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> : null } - { invite_link } - { team_link } - { manage_link } - { rename_link } - <li><a href="#" onClick={this.handleLogoutClick}>Logout</a></li> - { teams } - <li className="divider"></li> - <li><a target="_blank" href={config.HelpLink}>Help</a></li> - <li><a target="_blank" href={config.ReportProblemLink}>Report a Problem</a></li> + <ul className='dropdown-menu' role='menu'> + <li><a href='#' data-toggle='modal' data-target='#user_settings1'>Account Settings</a></li> + {teamSettings} + {inviteLink} + {teamLink} + {manageLink} + {renameLink} + <li><a href='#' onClick={this.handleLogoutClick}>Logout</a></li> + {teams} + <li className='divider'></li> + <li><a target='_blank' href={config.HelpLink}>Help</a></li> + <li><a target='_blank' href={config.ReportProblemLink}>Report a Problem</a></li> </ul> </li> </ul> @@ -109,14 +116,13 @@ var NavbarDropdown = React.createClass({ module.exports = React.createClass({ displayName: 'SidebarHeader', - getDefaultProps: function() { return { teamDisplayName: config.SiteName }; }, - toggleDropdown: function(e) { + toggleDropdown: function() { if (this.refs.dropdown.blockToggle) { this.refs.dropdown.blockToggle = false; return; @@ -126,25 +132,26 @@ module.exports = React.createClass({ render: function() { var me = UserStore.getCurrentUser(); + var profilePicture = null; if (!me) { return null; } + if (me.last_picture_update) { + profilePicture = (<img className='user__picture' src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at} />); + } + return ( - <div className="team__header theme"> - <a href="#" onClick={this.toggleDropdown}> - { me.last_picture_update ? - <img className="user__picture" src={"/api/v1/users/" + me.id + "/image?time=" + me.update_at} /> - : - null - } - <div className="header__info"> - <div className="user__name">{ '@' + me.username}</div> - <div className="team__name">{ this.props.teamDisplayName }</div> + <div className='team__header theme'> + <a href='#' onClick={this.toggleDropdown}> + {profilePicture} + <div className='header__info'> + <div className='user__name'>{'@' + me.username}</div> + <div className='team__name'>{this.props.teamDisplayName }</div> </div> </a> - <NavbarDropdown ref="dropdown" teamType={this.props.teamType} /> + <NavbarDropdown ref='dropdown' teamType={this.props.teamType} /> </div> ); } diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx index 15306a499..2439719a1 100644 --- a/web/react/components/sidebar_right_menu.jsx +++ b/web/react/components/sidebar_right_menu.jsx @@ -3,6 +3,7 @@ var UserStore = require('../stores/user_store.jsx'); var client = require('../utils/client.jsx'); +var utils = require('../utils/utils.jsx'); module.exports = React.createClass({ handleLogoutClick: function(e) { @@ -10,65 +11,77 @@ module.exports = React.createClass({ client.logout(); }, render: function() { - var team_link = ""; - var invite_link = ""; - var manage_link = ""; - var rename_link = ""; - var currentUser = UserStore.getCurrentUser() + var teamLink = ''; + var inviteLink = ''; + var teamSettingsLink = ''; + var manageLink = ''; + var renameLink = ''; + var currentUser = UserStore.getCurrentUser(); var isAdmin = false; if (currentUser != null) { - isAdmin = currentUser.roles.indexOf("admin") > -1; + isAdmin = currentUser.roles.indexOf('admin') > -1; - invite_link = ( + inviteLink = ( <li> - <a href="#" data-toggle="modal" data-target="#invite_member"><i className="glyphicon glyphicon-user"></i>Invite New Member</a> + <a href='#' data-toggle='modal' data-target='#invite_member'><i className='glyphicon glyphicon-user'></i>Invite New Member</a> </li> ); - if (this.props.teamType == "O") { - team_link = ( + if (this.props.teamType === 'O') { + teamLink = ( <li> - <a href="#" data-toggle="modal" data-target="#get_link" data-title="Team Invite" data-value={location.origin+"/signup_user_complete/?id="+currentUser.team_id}><i className="glyphicon glyphicon-link"></i>Get Team Invite Link</a> + <a href='#' data-toggle='modal' data-target='#get_link' data-title='Team Invite' data-value={utils.getWindowLocationOrigin()+'/signup_user_complete/?id='+currentUser.team_id}><i className='glyphicon glyphicon-link'></i>Get Team Invite Link</a> </li> ); } } if (isAdmin) { - manage_link = ( + teamSettingsLink = ( <li> - <a href="#" data-toggle="modal" data-target="#team_members"><i className="glyphicon glyphicon-wrench"></i>Manage Team</a> + <a href='#' data-toggle='modal' data-target='#team_settings'><i className='glyphicon glyphicon-globe'></i>Team Settings</a> </li> ); - rename_link = ( + manageLink = ( <li> - <a href="#" data-toggle="modal" data-target="#rename_team_link"><i className="glyphicon glyphicon-pencil"></i>Rename</a> + <a href='#' data-toggle='modal' data-target='#team_members'><i className='glyphicon glyphicon-wrench'></i>Manage Team</a> + </li> + ); + renameLink = ( + <li> + <a href='#' data-toggle='modal' data-target='#rename_team_link'><i className='glyphicon glyphicon-pencil'></i>Rename</a> </li> ); } - var siteName = config.SiteName != null ? config.SiteName : ""; - var teamDisplayName = this.props.teamDisplayName ? this.props.teamDisplayName : siteName; + var siteName = ''; + if (config.SiteName != null) { + siteName = config.SiteName; + } + var teamDisplayName = siteName; + if (this.props.teamDisplayName) { + teamDisplayName = this.props.teamDisplayName; + } return ( <div> - <div className="team__header theme"> - <a className="team__name" href="/channels/town-square">{ teamDisplayName }</a> + <div className='team__header theme'> + <a className='team__name' href='/channels/town-square'>{teamDisplayName}</a> </div> - <div className="nav-pills__container"> - <ul className="nav nav-pills nav-stacked"> - <li><a href="#" data-toggle="modal" data-target="#user_settings1"><i className="glyphicon glyphicon-cog"></i>Account Settings</a></li> - { isAdmin ? <li><a href="#" data-toggle="modal" data-target="#team_settings"><i className="glyphicon glyphicon-globe"></i>Team Settings</a></li> : "" } - { invite_link } - { team_link } - { manage_link } - { rename_link } - <li><a href="#" onClick={this.handleLogoutClick}><i className="glyphicon glyphicon-log-out"></i>Logout</a></li> - <li className="divider"></li> - <li><a target="_blank" href="/static/help/configure_links.html"><i className="glyphicon glyphicon-question-sign"></i>Help</a></li> - <li><a target="_blank" href="/static/help/configure_links.html"><i className="glyphicon glyphicon-earphone"></i>Report a Problem</a></li> + <div className='nav-pills__container'> + <ul className='nav nav-pills nav-stacked'> + <li><a href='#' data-toggle='modal' data-target='#user_settings1'><i className='glyphicon glyphicon-cog'></i>Account Settings</a></li> + {teamSettingsLink} + {inviteLink} + {teamLink} + {manageLink} + {renameLink} + <li><a href='#' onClick={this.handleLogoutClick}><i className='glyphicon glyphicon-log-out'></i>Logout</a></li> + <li className='divider'></li> + <li><a target='_blank' href='/static/help/configure_links.html'><i className='glyphicon glyphicon-question-sign'></i>Help</a></li> + <li><a target='_blank' href='/static/help/configure_links.html'><i className='glyphicon glyphicon-earphone'></i>Report a Problem</a></li> </ul> </div> </div> diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index 03808e821..b21553d8a 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -1,7 +1,6 @@ // 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'); @@ -9,36 +8,41 @@ var BrowserStore = require('../stores/browser_store.jsx'); module.exports = React.createClass({ handleSubmit: function(e) { - e.preventDefault(); + e.preventDefault(); this.state.user.username = this.refs.name.getDOMNode().value.trim(); if (!this.state.user.username) { - this.setState({name_error: "This field is required", email_error: "", password_error: "", server_error: ""}); + this.setState({nameError: 'This field is required', emailError: '', passwordError: '', serverError: ''}); return; } - var username_error = utils.isValidUsername(this.state.user.username); - if (username_error === "Cannot use a reserved word as a username.") { - this.setState({name_error: "This username is reserved, please choose a new one.", email_error: "", password_error: "", server_error: ""}); + var usernameError = utils.isValidUsername(this.state.user.username); + if (usernameError === 'Cannot use a reserved word as a username.') { + this.setState({nameError: 'This username is reserved, please choose a new one.', emailError: '', passwordError: '', serverError: ''}); return; - } else if (username_error) { - this.setState({name_error: "Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'.", email_error: "", password_error: "", server_error: ""}); + } else if (usernameError) { + this.setState({ + nameError: 'Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols \'.\', \'-\' and \'_\'.', + emailError: '', + passwordError: '', + serverError: '' + }); return; } this.state.user.email = this.refs.email.getDOMNode().value.trim(); if (!this.state.user.email) { - this.setState({name_error: "", email_error: "This field is required", password_error: ""}); + this.setState({nameError: '', emailError: 'This field is required', passwordError: ''}); return; } this.state.user.password = this.refs.password.getDOMNode().value.trim(); if (!this.state.user.password || this.state.user.password .length < 5) { - this.setState({name_error: "", email_error: "", password_error: "Please enter at least 5 characters", server_error: ""}); + this.setState({nameError: '', emailError: '', passwordError: 'Please enter at least 5 characters', serverError: ''}); return; } - this.setState({name_error: "", email_error: "", password_error: "", server_error: ""}); + this.setState({nameError: '', emailError: '', passwordError: '', serverError: ''}); this.state.user.allow_marketing = true; @@ -50,108 +54,154 @@ module.exports = React.createClass({ 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"})); + 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) + "&teamname=" + encodeURIComponent(this.props.teamName); + if (err.message === 'Login failed because email address has not been verified') { + window.location.href = '/verify_email?email=' + encodeURIComponent(this.state.user.email) + '&teamname=' + encodeURIComponent(this.props.teamName); } else { - this.state.server_error = err.message; - this.setState(this.state); + this.setState({serverError: err.message}); } }.bind(this) ); }.bind(this), function(err) { - this.state.server_error = err.message; - this.setState(this.state); + this.setState({serverError: err.message}); }.bind(this) ); }, getInitialState: function() { - var props = BrowserStore.getGlobalItem(this.props.hash); - - if (!props) { - props = {}; - props.wizard = "welcome"; - props.user = {}; - props.user.team_id = this.props.teamId; - props.user.email = this.props.email; - props.hash = this.props.hash; - props.data = this.props.data; - props.original_email = this.props.email; + var state = BrowserStore.getGlobalItem(this.props.hash); + + if (!state) { + state = {}; + state.wizard = 'welcome'; + state.user = {}; + state.user.team_id = this.props.teamId; + state.user.email = this.props.email; + state.hash = this.props.hash; + state.data = this.props.data; + state.original_email = this.props.email; } - return props; + return state; }, render: function() { - client.track('signup', 'signup_user_01_welcome'); - if (this.state.wizard == "finished") { - return (<div>You've already completed the signup process for this invitation or this invitation has expired.</div>); + if (this.state.wizard === 'finished') { + return <div>You've already completed the signup process for this invitation or this invitation has expired.</div>; + } + + // set up error labels + var emailError = null; + var emailDivStyle = 'form-group'; + if (this.state.emailError) { + emailError = <label className='control-label'>{this.state.emailError}</label>; + emailDivStyle += ' has-error'; + } + + var nameError = null; + var nameDivStyle = 'form-group'; + if (this.state.nameError) { + nameError = <label className='control-label'>{this.state.nameError}</label>; + nameDivStyle += ' has-error'; + } + + var passwordError = null; + var passwordDivStyle = 'form-group'; + if (this.state.passwordError) { + passwordError = <label className='control-label'>{this.state.passwordError}</label>; + passwordDivStyle += ' has-error'; + } + + var serverError = null; + if (this.state.serverError) { + serverError = ( + <div className={'form-group has-error'}> + <label className='control-label'>{this.state.serverError}</label> + </div> + ); } - var email_error = this.state.email_error ? <label className='control-label'>{ this.state.email_error }</label> : null; - var name_error = this.state.name_error ? <label className='control-label'>{ this.state.name_error }</label> : null; - var password_error = this.state.password_error ? <label className='control-label'>{ this.state.password_error }</label> : null; - var server_error = this.state.server_error ? <div className={ "form-group has-error" }><label className='control-label'>{ this.state.server_error }</label></div> : null; + // set up the email entry and hide it if an email was provided + var yourEmailIs = ''; + if (this.state.user.email) { + yourEmailIs = <span>Your email address is {this.state.user.email}. You'll use this address to sign in to {config.SiteName}.</span>; + } - var yourEmailIs = this.state.user.email == "" ? "" : <span>Your email address is { this.state.user.email }. </span> + var emailContainerStyle = "margin--extra"; + if (this.state.original_email) { + emailContainerStyle = "hidden"; + } var email = ( - <div className={ this.state.original_email == "" ? "margin--extra" : "hidden"} > + <div className={emailContainerStyle}> <h5><strong>What's your email address?</strong></h5> - <div className={ email_error ? "form-group has-error" : "form-group" }> - <input type="email" ref="email" className="form-control" defaultValue={ this.state.user.email } placeholder="" maxLength="128" autoFocus={true} /> - { email_error } - </div> + <div className={emailDivStyle}> + <input type='email' ref='email' className='form-control' defaultValue={this.state.user.email} placeholder='' maxLength='128' autoFocus={true} /> + {emailError} </div> + </div> ); - var auth_services = JSON.parse(this.props.authServices); + // add options to log in using another service + var authServices = JSON.parse(this.props.authServices); + + var signupMessage = null; + if (authServices.indexOf('gitlab') >= 0) { + signupMessage = ( + <div> + <a className='btn btn-custom-login gitlab' href={'/' + this.props.teamName + '/signup/gitlab' + window.location.search}> + <span className='icon' /> + <span>with GitLab</span> + </a> + <div className='or__container'> + <span>or</span> + </div> + </div> + ); + } - var signup_message; - if (auth_services.indexOf("gitlab") >= 0) { - signup_message = <div><a className="btn btn-custom-login gitlab" href={"/"+this.props.teamName+"/signup/gitlab"+window.location.search}><span className="icon" />{"with GitLab"}</a> - <div className="or__container"><span>or</span></div></div>; + var termsDisclaimer = null; + if (config.ShowTermsDuringSignup) { + termsDisclaimer = <p>By creating an account and using Mattermost you are agreeing to our <a href={config.TermsLink}>Terms of Service</a>. If you do not agree, you cannot use this service.</p>; } return ( <div> <form> - <img className="signup-team-logo" src="/static/images/logo.png" /> - <h5 className="margin--less">Welcome to:</h5> - <h2 className="signup-team__name">{ this.props.teamDisplayName }</h2> - <h2 className="signup-team__subdomain">on { config.SiteName }</h2> - <h4 className="color--light">Let's create your account</h4> - { signup_message } - <div className="inner__content"> - { email } - <p className={ this.state.original_email == "" ? "hidden" : ""}>{ yourEmailIs } You’ll use this address to sign in to {config.SiteName}.</p> - <div className="margin--extra"> - <h5><strong>Choose your username</strong></h5> - <div className={ name_error ? "form-group has-error" : "form-group" }> - <input type="text" ref="name" className="form-control" placeholder="" maxLength="128" /> - { name_error } - <p className="form__hint">Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'"</p> + <img className='signup-team-logo' src='/static/images/logo.png' /> + <h5 className='margin--less'>Welcome to:</h5> + <h2 className='signup-team__name'>{this.props.teamDisplayName}</h2> + <h2 className='signup-team__subdomain'>on {config.SiteName}</h2> + <h4 className='color--light'>Let's create your account</h4> + {signupMessage} + <div className='inner__content'> + {email} + {yourEmailIs} + <div className='margin--extra'> + <h5><strong>Choose your username</strong></h5> + <div className={nameDivStyle}> + <input type='text' ref='name' className='form-control' placeholder='' maxLength='128' /> + {nameError} + <p className='form__hint'>Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'</p> + </div> + </div> + <div className='margin--extra'> + <h5><strong>Choose your password</strong></h5> + <div className={passwordDivStyle}> + <input type='password' ref='password' className='form-control' placeholder='' maxLength='128' /> + {passwordError} </div> - </div> - <div className="margin--extra"> - <h5><strong>Choose your password</strong></h5> - <div className={ password_error ? "form-group has-error" : "form-group" }> - <input type="password" ref="password" className="form-control" placeholder="" maxLength="128" /> - { password_error } </div> </div> - </div> - <p className="margin--extra"><button type='submit' onClick={this.handleSubmit} className="btn-primary btn">Create Account</button></p> - { server_error } - <p>By creating an account and using Mattermost you are agreeing to our <a href={ config.TermsLink }>Terms of Service</a>. If you do not agree, you cannot use this service.</p> + <p className='margin--extra'><button type='submit' onClick={this.handleSubmit} className='btn-primary btn'>Create Account</button></p> + {serverError} + {termsDisclaimer} </form> </div> ); diff --git a/web/react/components/team_settings_modal.jsx b/web/react/components/team_settings_modal.jsx index e50378b7f..b1c38fd16 100644 --- a/web/react/components/team_settings_modal.jsx +++ b/web/react/components/team_settings_modal.jsx @@ -29,7 +29,7 @@ module.exports = React.createClass({ tabs.push({name: "feature", ui_name: "Features", icon: "glyphicon glyphicon-wrench"}); return ( - <div className="modal fade" ref="modal" id="team_settings" role="dialog" aria-hidden="true"> + <div className="modal fade" ref="modal" id="team_settings" role="dialog" tabIndex="-1" aria-hidden="true"> <div className="modal-dialog settings-modal"> <div className="modal-content"> <div className="modal-header"> diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx index bbd1f84b6..b5c5cc564 100644 --- a/web/react/components/textbox.jsx +++ b/web/react/components/textbox.jsx @@ -2,11 +2,7 @@ // See License.txt for license information. var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -var UserStore = require('../stores/user_store.jsx'); var PostStore = require('../stores/post_store.jsx'); -var SocketStore = require('../stores/socket_store.jsx'); -var MsgTyping = require('./msg_typing.jsx'); -var MentionList = require('./mention_list.jsx'); var CommandList = require('./command_list.jsx'); var ErrorStore = require('../stores/error_store.jsx'); var AsyncClient = require('../utils/async_client.jsx'); @@ -19,53 +15,53 @@ function getStateFromStores() { var error = ErrorStore.getLastError(); if (error) { - return { message: error.message }; - } else { - return { message: null }; + return {message: error.message}; } + return {message: null}; } module.exports = React.createClass({ + displayName: 'Textbox', caret: -1, addedMention: false, doProcessMentions: false, mentions: [], componentDidMount: function() { - PostStore.addAddMentionListener(this._onChange); - ErrorStore.addChangeListener(this._onError); + PostStore.addAddMentionListener(this.onListenerChange); + ErrorStore.addChangeListener(this.onRecievedError); this.resize(); - this.processMentions(); + this.updateMentionTab(null); }, componentWillUnmount: function() { - PostStore.removeAddMentionListener(this._onChange); - ErrorStore.removeChangeListener(this._onError); + PostStore.removeAddMentionListener(this.onListenerChange); + ErrorStore.removeChangeListener(this.onRecievedError); }, - _onChange: function(id, username) { - if (id !== this.props.id) return; - this.addMention(username); + onListenerChange: function(id, username) { + if (id === this.props.id) { + this.addMention(username); + } }, - _onError: function() { + onRecievedError: function() { var errorState = getStateFromStores(); if (this.state.timerInterrupt != null) { window.clearInterval(this.state.timerInterrupt); - this.setState({ timerInterrupt: null }); + this.setState({timerInterrupt: null}); } - if (errorState.message === "There appears to be a problem with your internet connection") { - this.setState({ connection: "bad-connection" }); - var timerInterrupt = window.setInterval(this._onTimerInterrupt, 5000); - this.setState({ timerInterrupt: timerInterrupt }); - } - else { - this.setState({ connection: "" }); + if (errorState.message === 'There appears to be a problem with your internet connection') { + this.setState({connection: 'bad-connection'}); + var timerInterrupt = window.setInterval(this.onTimerInterrupt, 5000); + this.setState({timerInterrupt: timerInterrupt}); + } else { + this.setState({connection: ''}); } }, - _onTimerInterrupt: function() { + onTimerInterrupt: function() { //Since these should only happen when you have no connection and slightly briefly after any //performance hit should not matter - if (this.state.connection === "bad-connection") { + if (this.state.connection === 'bad-connection') { AppDispatcher.handleServerAction({ type: ActionTypes.RECIEVED_ERROR, err: null @@ -75,15 +71,15 @@ module.exports = React.createClass({ } window.clearInterval(this.state.timerInterrupt); - this.setState({ timerInterrupt: null }); + this.setState({timerInterrupt: null}); }, componentDidUpdate: function() { if (this.caret >= 0) { - utils.setCaretPosition(this.refs.message.getDOMNode(), this.caret) + utils.setCaretPosition(this.refs.message.getDOMNode(), this.caret); this.caret = -1; } if (this.doProcessMentions) { - this.processMentions(); + this.updateMentionTab(null); this.doProcessMentions = false; } this.resize(); @@ -93,7 +89,7 @@ module.exports = React.createClass({ this.checkForNewMention(nextProps.messageText); } var text = this.refs.message.getDOMNode().value; - if (nextProps.channelId != this.props.channelId || nextProps.messageText !== text) { + if (nextProps.channelId !== this.props.channelId || nextProps.messageText !== text) { this.doProcessMentions = true; } this.addedMention = false; @@ -101,17 +97,17 @@ module.exports = React.createClass({ this.resize(); }, getInitialState: function() { - return { mentionText: '-1', mentions: [], connection: "", timerInterrupt: null }; + return {mentionText: '-1', mentions: [], connection: '', timerInterrupt: null}; }, - updateMentionTab: function(mentionText, excludeList) { + updateMentionTab: function(mentionText) { var self = this; + // using setTimeout so dispatch isn't called during an in progress dispatch setTimeout(function() { AppDispatcher.handleViewAction({ type: ActionTypes.RECIEVED_MENTION_DATA, id: self.props.id, - mention_text: mentionText, - exclude_list: excludeList + mention_text: mentionText }); }, 1); }, @@ -122,13 +118,13 @@ module.exports = React.createClass({ handleKeyPress: function(e) { var text = this.refs.message.getDOMNode().value; - if (!this.refs.commands.isEmpty() && text.indexOf("/") == 0 && e.which==13) { + if (!this.refs.commands.isEmpty() && text.indexOf('/') === 0 && e.which === 13) { this.refs.commands.addFirstCommand(); e.preventDefault(); return; } - if ( !this.doProcessMentions) { + if (!this.doProcessMentions) { var caret = utils.getCaretPosition(this.refs.message.getDOMNode()); var preText = text.substring(0, caret); var lastSpace = preText.lastIndexOf(' '); @@ -150,13 +146,15 @@ module.exports = React.createClass({ this.handleBackspace(e); } }, - handleBackspace: function(e) { + handleBackspace: function() { var text = this.refs.message.getDOMNode().value; - if (text.indexOf("/") == 0) { - this.refs.commands.getSuggestedCommands(text.substring(0, text.length-1)); + if (text.indexOf('/') === 0) { + this.refs.commands.getSuggestedCommands(text.substring(0, text.length - 1)); } - if (this.doProcessMentions) return; + if (this.doProcessMentions) { + return; + } var caret = utils.getCaretPosition(this.refs.message.getDOMNode()); var preText = text.substring(0, caret); @@ -167,57 +165,6 @@ module.exports = React.createClass({ this.doProcessMentions = true; } }, - processMentions: function() { - /* First, find all the possible mentions and add - them all to a list of mentions */ - var text = utils.insertHtmlEntities(this.refs.message.getDOMNode().value); - - var profileMap = UserStore.getProfilesUsernameMap(); - - var re1 = /@([a-z0-9_]+)( |$|\n)/gi; - - var matches = text.match(re1); - - if (!matches) { - this.updateMentionTab(null, []); - return; - } - - var mentions = []; - for (var i = 0; i < matches.length; i++) { - var m = matches[i].substring(1,matches[i].length).trim(); - if ((m in profileMap && mentions.indexOf(m) === -1) || Constants.SPECIAL_MENTIONS.indexOf(m) !== -1) { - mentions.push(m); - } - } - - /* Figure out what the user is currently typing. If it's a mention then we don't - want to add it to the mention list yet, so we remove it if - there is only one occurence of that mention so far. */ - var caret = utils.getCaretPosition(this.refs.message.getDOMNode()); - - var text = this.props.messageText; - - var preText = text.substring(0, caret); - - var atIndex = preText.lastIndexOf('@'); - var spaceIndex = preText.lastIndexOf(' '); - var newLineIndex = preText.lastIndexOf('\n'); - - var typingMention = ""; - if (atIndex > spaceIndex && atIndex > newLineIndex) { - - typingMention = text.substring(atIndex+1, caret); - } - - var re2 = new RegExp('@' + typingMention + '( |$|\n)', 'g'); - - if ((text.match(re2) || []).length === 1 && mentions.indexOf(typingMention) !== -1) { - mentions.splice(mentions.indexOf(typingMention), 1); - } - - this.updateMentionTab(null, mentions); - }, checkForNewMention: function(text) { var caret = utils.getCaretPosition(this.refs.message.getDOMNode()); @@ -227,7 +174,7 @@ module.exports = React.createClass({ // The @ character not typed, so nothing to do. if (atIndex === -1) { - this.updateMentionTab('-1', null); + this.updateMentionTab('-1'); return; } @@ -236,13 +183,13 @@ module.exports = React.createClass({ // If there is a space after the last @, nothing to do. if (lastSpace > atIndex || lastCharSpace > atIndex) { - this.updateMentionTab('-1', null); + this.updateMentionTab('-1'); return; } // Get the name typed so far. - var name = preText.substring(atIndex+1, preText.length).toLowerCase(); - this.updateMentionTab(name, null); + var name = preText.substring(atIndex + 1, preText.length).toLowerCase(); + this.updateMentionTab(name); }, addMention: function(name) { var caret = utils.getCaretPosition(this.refs.message.getDOMNode()); @@ -264,7 +211,7 @@ module.exports = React.createClass({ this.addedMention = true; this.doProcessMentions = true; - this.props.onUserInput(prefix + "@" + name + " " + suffix); + this.props.onUserInput(prefix + '@' + name + ' ' + suffix); }, addCommand: function(cmd) { var elm = this.refs.message.getDOMNode(); @@ -275,22 +222,26 @@ module.exports = React.createClass({ var e = this.refs.message.getDOMNode(); var w = this.refs.wrapper.getDOMNode(); - var lht = parseInt($(e).css('lineHeight'),10); + var lht = parseInt($(e).css('lineHeight'), 10); var lines = e.scrollHeight / lht; - var mod = lines < 2.5 || this.props.messageText === "" ? 30 : 15; + var mod = 15; + + if (lines < 2.5 || this.props.messageText === '') { + mod = 30; + } if (e.scrollHeight - mod < 167) { - $(e).css({'height':'auto','overflow-y':'hidden'}).height(e.scrollHeight - mod); - $(w).css({'height':'auto'}).height(e.scrollHeight+2); + $(e).css({height: 'auto', 'overflow-y': 'hidden'}).height(e.scrollHeight - mod); + $(w).css({height: 'auto'}).height(e.scrollHeight + 2); } else { - $(e).css({'height':'auto','overflow-y':'scroll'}).height(167); - $(w).css({'height':'auto'}).height(167); + $(e).css({height: 'auto', 'overflow-y': 'scroll'}).height(167); + $(w).css({height: 'auto'}).height(167); } }, handleFocus: function() { var elm = this.refs.message.getDOMNode(); if (elm.title === elm.value) { - elm.value = ""; + elm.value = ''; } }, handleBlur: function() { @@ -304,9 +255,9 @@ module.exports = React.createClass({ }, render: function() { return ( - <div ref="wrapper" className="textarea-wrapper"> + <div ref='wrapper' className='textarea-wrapper'> <CommandList ref='commands' addCommand={this.addCommand} channelId={this.props.channelId} /> - <textarea id={this.props.id} ref="message" className={"form-control custom-textarea " + this.state.connection} spellCheck="true" autoComplete="off" autoCorrect="off" rows="1" placeholder={this.props.createMessage} value={this.props.messageText} onInput={this.handleChange} onChange={this.handleChange} onKeyPress={this.handleKeyPress} onKeyDown={this.handleKeyDown} onFocus={this.handleFocus} onBlur={this.handleBlur} onPaste={this.handlePaste} /> + <textarea id={this.props.id} ref='message' className={'form-control custom-textarea ' + this.state.connection} spellCheck='true' autoComplete='off' autoCorrect='off' rows='1' placeholder={this.props.createMessage} value={this.props.messageText} onInput={this.handleChange} onChange={this.handleChange} onKeyPress={this.handleKeyPress} onKeyDown={this.handleKeyDown} onFocus={this.handleFocus} onBlur={this.handleBlur} onPaste={this.handlePaste} /> </div> ); } diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx index 65f025919..5c4d26a23 100644 --- a/web/react/components/user_profile.jsx +++ b/web/react/components/user_profile.jsx @@ -28,6 +28,7 @@ module.exports = React.createClass({ componentDidMount: function() { UserStore.addChangeListener(this._onChange); $("#profile_" + this.uniqueId).popover({placement : 'right', container: 'body', trigger: 'hover', html: true, delay: { "show": 200, "hide": 100 }}); + $('body').tooltip( {selector: '[data-toggle=tooltip]', trigger: 'hover click'} ); }, componentWillUnmount: function() { UserStore.removeChangeListener(this._onChange); @@ -57,7 +58,7 @@ module.exports = React.createClass({ if (!config.ShowEmail) { data_content += "<div class='text-nowrap'>Email not shared</div>"; } else { - data_content += "<div><a href='mailto:" + this.state.profile.email + "' class='text-nowrap text-lowercase'>" + this.state.profile.email + "</a></div>"; + data_content += "<div data-toggle='tooltip' title= '" + this.state.profile.email + "'><a href='mailto:" + this.state.profile.email + "' class='text-nowrap text-lowercase user-popover__email'>" + this.state.profile.email + "</a></div>"; } return ( diff --git a/web/react/components/user_settings.jsx b/web/react/components/user_settings.jsx index 95d1178d1..1a0c313d3 100644 --- a/web/react/components/user_settings.jsx +++ b/web/react/components/user_settings.jsx @@ -5,8 +5,6 @@ var UserStore = require('../stores/user_store.jsx'); var SettingItemMin = require('./setting_item_min.jsx'); var SettingItemMax = require('./setting_item_max.jsx'); var SettingPicture = require('./setting_picture.jsx'); -var AccessHistoryModal = require('./access_history_modal.jsx'); -var ActivityLogModal = require('./activity_log_modal.jsx'); var client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var utils = require('../utils/utils.jsx'); @@ -102,6 +100,8 @@ var NotificationsTab = React.createClass({ }); this.setState(assign({},getNotificationsStateFromStores(),{server_error: null})); + + this.props.updateTab('general'); }, componentDidMount: function() { UserStore.addChangeListener(this._onChange); @@ -110,6 +110,7 @@ var NotificationsTab = React.createClass({ componentWillUnmount: function() { UserStore.removeChangeListener(this._onChange); $('#user_settings1').off('hidden.bs.modal', this.handleClose); + this.props.updateSection(''); }, _onChange: function() { var newState = getNotificationsStateFromStores(); @@ -460,22 +461,22 @@ var SecurityTab = React.createClass({ e.preventDefault(); var user = this.props.user; - var currentPassword = this.state.current_password; - var newPassword = this.state.new_password; - var confirmPassword = this.state.confirm_password; + var currentPassword = this.state.currentPassword; + var newPassword = this.state.newPassword; + var confirmPassword = this.state.confirmPassword; if (currentPassword === '') { - this.setState({password_error: 'Please enter your current password', server_error: ''}); + this.setState({passwordError: 'Please enter your current password', serverError: ''}); return; } if (newPassword.length < 5) { - this.setState({password_error: 'New passwords must be at least 5 characters', server_error: ''}); + this.setState({passwordError: 'New passwords must be at least 5 characters', serverError: ''}); return; } if (newPassword !== confirmPassword) { - this.setState({password_error: 'The new passwords you entered do not match', server_error: ''}); + this.setState({passwordError: 'The new passwords you entered do not match', serverError: ''}); return; } @@ -485,85 +486,88 @@ var SecurityTab = React.createClass({ data.new_password = newPassword; client.updatePassword(data, - function(data) { - this.props.updateSection(""); + function() { + this.props.updateSection(''); AsyncClient.getMe(); - this.setState({current_password: '', new_password: '', confirm_password: ''}); + this.setState({currentPassword: '', newPassword: '', confirmPassword: ''}); }.bind(this), function(err) { var state = this.getInitialState(); if (err.message) { - state.server_error = err.message; + state.serverError = err.message; } else { - state.server_error = err; + state.serverError = err; } - state.password_error = ''; + state.passwordError = ''; this.setState(state); }.bind(this) ); }, updateCurrentPassword: function(e) { - this.setState({ current_password: e.target.value }); + this.setState({currentPassword: e.target.value}); }, updateNewPassword: function(e) { - this.setState({ new_password: e.target.value }); + this.setState({newPassword: e.target.value}); }, updateConfirmPassword: function(e) { - this.setState({ confirm_password: e.target.value }); + this.setState({confirmPassword: e.target.value}); }, handleHistoryOpen: function() { - $("#user_settings1").modal('hide'); + $('#user_settings1').modal('hide'); }, handleDevicesOpen: function() { - $("#user_settings1").modal('hide'); + $('#user_settings1').modal('hide'); }, handleClose: function() { - $(this.getDOMNode()).find(".form-control").each(function() { - this.value = ""; + $(this.getDOMNode()).find('.form-control').each(function() { + this.value = ''; }); - this.setState({current_password: '', new_password: '', confirm_password: '', server_error: null, password_error: null}); + this.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null}); + this.props.updateTab('general'); }, componentDidMount: function() { $('#user_settings1').on('hidden.bs.modal', this.handleClose); }, componentWillUnmount: function() { $('#user_settings1').off('hidden.bs.modal', this.handleClose); + this.props.updateSection(''); }, getInitialState: function() { - return { current_password: '', new_password: '', confirm_password: '' }; + return {currentPassword: '', newPassword: '', confirmPassword: ''}; }, render: function() { - var server_error = this.state.server_error ? this.state.server_error : null; - var password_error = this.state.password_error ? this.state.password_error : null; + var serverError = this.state.serverError ? this.state.serverError : null; + var passwordError = this.state.passwordError ? this.state.passwordError : null; + var updateSectionStatus; var passwordSection; var self = this; if (this.props.activeSection === 'password') { var inputs = []; var submit = null; - if (this.props.user.auth_service === "") { + if (this.props.user.auth_service === '') { inputs.push( - <div className="form-group"> - <label className="col-sm-5 control-label">Current Password</label> - <div className="col-sm-7"> - <input className="form-control" type="password" onChange={this.updateCurrentPassword} value={this.state.current_password}/> + <div className='form-group'> + <label className='col-sm-5 control-label'>Current Password</label> + <div className='col-sm-7'> + <input className='form-control' type='password' onChange={this.updateCurrentPassword} value={this.state.currentPassword}/> </div> </div> ); inputs.push( - <div className="form-group"> - <label className="col-sm-5 control-label">New Password</label> - <div className="col-sm-7"> - <input className="form-control" type="password" onChange={this.updateNewPassword} value={this.state.new_password}/> + <div className='form-group'> + <label className='col-sm-5 control-label'>New Password</label> + <div className='col-sm-7'> + <input className='form-control' type='password' onChange={this.updateNewPassword} value={this.state.newPassword}/> </div> </div> ); inputs.push( - <div className="form-group"> - <label className="col-sm-5 control-label">Retype New Password</label> - <div className="col-sm-7"> - <input className="form-control" type="password" onChange={this.updateConfirmPassword} value={this.state.confirm_password}/> + <div className='form-group'> + <label className='col-sm-5 control-label'>Retype New Password</label> + <div className='col-sm-7'> + <input className='form-control' type='password' onChange={this.updateConfirmPassword} value={this.state.confirmPassword}/> </div> </div> ); @@ -571,58 +575,68 @@ var SecurityTab = React.createClass({ submit = this.submitPassword; } else { inputs.push( - <div className="form-group"> - <label className="col-sm-12">Log in occurs through GitLab. Please see your GitLab account settings page to update your password.</label> + <div className='form-group'> + <label className='col-sm-12'>Log in occurs through GitLab. Please see your GitLab account settings page to update your password.</label> </div> ); } + updateSectionStatus = function(e) { + self.props.updateSection(''); + self.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null}); + e.preventDefault(); + }; + passwordSection = ( <SettingItemMax - title="Password" + title='Password' inputs={inputs} submit={submit} - server_error={server_error} - client_error={password_error} - updateSection={function(e){self.props.updateSection("");e.preventDefault();}} + server_error={serverError} + client_error={passwordError} + updateSection={updateSectionStatus} /> ); } else { var describe; - if (this.props.user.auth_service === "") { + if (this.props.user.auth_service === '') { var d = new Date(this.props.user.last_password_update); - var hour = d.getHours() % 12 ? String(d.getHours() % 12) : "12"; - var min = d.getMinutes() < 10 ? "0" + d.getMinutes() : String(d.getMinutes()); - var timeOfDay = d.getHours() >= 12 ? " pm" : " am"; - describe = "Last updated " + Constants.MONTHS[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear() + " at " + hour + ":" + min + timeOfDay; + var hour = d.getHours() % 12 ? String(d.getHours() % 12) : '12'; + var min = d.getMinutes() < 10 ? '0' + d.getMinutes() : String(d.getMinutes()); + var timeOfDay = d.getHours() >= 12 ? ' pm' : ' am'; + describe = 'Last updated ' + Constants.MONTHS[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear() + ' at ' + hour + ':' + min + timeOfDay; } else { - describe = "Log in done through GitLab" + describe = 'Log in done through GitLab'; } + updateSectionStatus = function() { + self.props.updateSection('password'); + }; + passwordSection = ( <SettingItemMin - title="Password" + title='Password' describe={describe} - updateSection={function(){self.props.updateSection("password");}} + updateSection={updateSectionStatus} /> ); } return ( <div> - <div className="modal-header"> - <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> - <h4 className="modal-title" ref="title"><i className="modal-back"></i>Security Settings</h4> + <div className='modal-header'> + <button type='button' className='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>×</span></button> + <h4 className='modal-title' ref='title'><i className='modal-back'></i>Security Settings</h4> </div> - <div className="user-settings"> - <h3 className="tab-header">Security Settings</h3> - <div className="divider-dark first"/> - { passwordSection } - <div className="divider-dark"/> + <div className='user-settings'> + <h3 className='tab-header'>Security Settings</h3> + <div className='divider-dark first'/> + {passwordSection} + <div className='divider-dark'/> <br></br> - <a data-toggle="modal" className="security-links theme" data-target="#access-history" href="#" onClick={this.handleHistoryOpen}><i className="fa fa-clock-o"></i>View Access History</a> - <b> </b> - <a data-toggle="modal" className="security-links theme" data-target="#activity-log" href="#" onClick={this.handleDevicesOpen}><i className="fa fa-globe"></i>View and Logout of Active Sessions</a> + <a data-toggle='modal' className='security-links theme' data-target='#access-history' href='#' onClick={this.handleHistoryOpen}><i className='fa fa-clock-o'></i>View Access History</a> + <b> </b> + <a data-toggle='modal' className='security-links theme' data-target='#activity-log' href='#' onClick={this.handleDevicesOpen}><i className='fa fa-globe'></i>View and Logout of Active Sessions</a> </div> </div> ); @@ -637,17 +651,17 @@ var GeneralTab = React.createClass({ var user = this.props.user; var username = this.state.username.trim(); - var username_error = utils.isValidUsername(username); - if (username_error === 'Cannot use a reserved word as a username.') { - this.setState({client_error: 'This username is reserved, please choose a new one.' }); + var usernameError = utils.isValidUsername(username); + if (usernameError === 'Cannot use a reserved word as a username.') { + this.setState({clientError: 'This username is reserved, please choose a new one.'}); return; - } else if (username_error) { - this.setState({client_error: "Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'." }); + } else if (usernameError) { + this.setState({clientError: "Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'."}); return; } if (user.username === username) { - this.setState({client_error: 'You must submit a new username'}); + this.setState({clientError: 'You must submit a new username'}); return; } @@ -662,7 +676,7 @@ var GeneralTab = React.createClass({ var nickname = this.state.nickname.trim(); if (user.nickname === nickname) { - this.setState({client_error: 'You must submit a new nickname'}) + this.setState({clientError: 'You must submit a new nickname'}); return; } @@ -674,11 +688,11 @@ var GeneralTab = React.createClass({ e.preventDefault(); var user = UserStore.getCurrentUser(); - var firstName = this.state.first_name.trim(); - var lastName = this.state.last_name.trim(); + var firstName = this.state.firstName.trim(); + var lastName = this.state.lastName.trim(); if (user.first_name === firstName && user.last_name === lastName) { - this.setState({client_error: 'You must submit a new first or last name'}) + this.setState({clientError: 'You must submit a new first or last name'}); return; } @@ -698,7 +712,7 @@ var GeneralTab = React.createClass({ } if (email === '' || !utils.isEmail(email)) { - this.setState({ email_error: 'Please enter a valid email address' }); + this.setState({emailError: 'Please enter a valid email address'}); return; } @@ -713,11 +727,11 @@ var GeneralTab = React.createClass({ AsyncClient.getMe(); }.bind(this), function(err) { - state = this.getInitialState(); + var state = this.getInitialState(); if (err.message) { - state.server_error = err.message; + state.serverError = err.message; } else { - state.server_error = err; + state.serverError = err; } this.setState(state); }.bind(this) @@ -737,12 +751,13 @@ var GeneralTab = React.createClass({ var picture = this.state.picture; if (picture.type !== 'image/jpeg' && picture.type !== 'image/png') { - this.setState({client_error: 'Only JPG or PNG images may be used for profile pictures'}); + this.setState({clientError: 'Only JPG or PNG images may be used for profile pictures'}); return; } var formData = new FormData(); formData.append('image', picture, picture.name); + this.setState({loadingPicture: true}); client.uploadProfileImage(formData, function() { @@ -751,8 +766,8 @@ var GeneralTab = React.createClass({ window.location.reload(); }.bind(this), function(err) { - state = this.getInitialState(); - state.server_error = err; + var state = this.getInitialState(); + state.serverError = err; this.setState(state); }.bind(this) ); @@ -761,10 +776,10 @@ var GeneralTab = React.createClass({ this.setState({username: e.target.value}); }, updateFirstName: function(e) { - this.setState({first_name: e.target.value}); + this.setState({firstName: e.target.value}); }, updateLastName: function(e) { - this.setState({last_name: e.target.value}); + this.setState({lastName: e.target.value}); }, updateNickname: function(e) { this.setState({nickname: e.target.value}); @@ -774,17 +789,16 @@ var GeneralTab = React.createClass({ }, updatePicture: function(e) { if (e.target.files && e.target.files[0]) { - this.setState({ picture: e.target.files[0] }); + this.setState({picture: e.target.files[0]}); this.submitActive = true; - this.setState({client_error: null}); - + this.setState({clientError: null}); } else { this.setState({picture: null}); } }, updateSection: function(section) { - this.setState({client_error:''}); + this.setState({clientError: ''}); this.submitActive = false; this.props.updateSection(section); }, @@ -793,7 +807,8 @@ var GeneralTab = React.createClass({ this.value = ''; }); - this.setState(assign({}, this.getInitialState(), {client_error: null, server_error: null, email_error: null})); + this.setState(assign({}, this.getInitialState(), {clientError: null, serverError: null, emailError: null})); + this.props.updateSection(''); }, componentDidMount: function() { $('#user_settings1').on('hidden.bs.modal', this.handleClose); @@ -804,15 +819,24 @@ var GeneralTab = React.createClass({ getInitialState: function() { var user = this.props.user; - return { username: user.username, first_name: user.first_name, last_name: user.last_name, nickname: user.nickname, - email: user.email, picture: null }; + return {username: user.username, firstName: user.first_name, lastName: user.last_name, nickname: user.nickname, + email: user.email, picture: null, loadingPicture: false}; }, render: function() { var user = this.props.user; - var client_error = this.state.client_error ? this.state.client_error : null; - var server_error = this.state.server_error ? this.state.server_error : null; - var email_error = this.state.email_error ? this.state.email_error : null; + var clientError = null; + if (this.state.clientError) { + clientError = this.state.clientError; + } + var serverError = null; + if (this.state.serverError) { + serverError = this.state.serverError; + } + var emailError = null; + if (this.state.emailError) { + emailError = this.state.emailError; + } var nameSection; var self = this; @@ -823,7 +847,7 @@ var GeneralTab = React.createClass({ <div className='form-group'> <label className='col-sm-5 control-label'>First Name</label> <div className='col-sm-7'> - <input className='form-control' type='text' onChange={this.updateFirstName} value={this.state.first_name}/> + <input className='form-control' type='text' onChange={this.updateFirstName} value={this.state.firstName}/> </div> </div> ); @@ -832,7 +856,7 @@ var GeneralTab = React.createClass({ <div className='form-group'> <label className='col-sm-5 control-label'>Last Name</label> <div className='col-sm-7'> - <input className='form-control' type='text' onChange={this.updateLastName} value={this.state.last_name}/> + <input className='form-control' type='text' onChange={this.updateLastName} value={this.state.lastName}/> </div> </div> ); @@ -842,8 +866,8 @@ var GeneralTab = React.createClass({ title='Full Name' inputs={inputs} submit={this.submitName} - server_error={server_error} - client_error={client_error} + server_error={serverError} + client_error={clientError} updateSection={function(e) { self.updateSection(''); e.preventDefault(); @@ -851,20 +875,20 @@ var GeneralTab = React.createClass({ /> ); } else { - var full_name = ''; + var fullName = ''; if (user.first_name && user.last_name) { - full_name = user.first_name + ' ' + user.last_name; + fullName = user.first_name + ' ' + user.last_name; } else if (user.first_name) { - full_name = user.first_name; + fullName = user.first_name; } else if (user.last_name) { - full_name = user.last_name; + fullName = user.last_name; } nameSection = ( <SettingItemMin title='Full Name' - describe={full_name} + describe={fullName} updateSection={function() { self.updateSection('name'); }} @@ -874,7 +898,6 @@ var GeneralTab = React.createClass({ var nicknameSection; if (this.props.activeSection === 'nickname') { - inputs.push( <div className='form-group'> <label className='col-sm-5 control-label'>{utils.isMobile() ? '' : 'Nickname'}</label> @@ -889,8 +912,8 @@ var GeneralTab = React.createClass({ title='Nickname' inputs={inputs} submit={this.submitNickname} - server_error={server_error} - client_error={client_error} + server_error={serverError} + client_error={clientError} updateSection={function(e) { self.updateSection(''); e.preventDefault(); @@ -913,7 +936,7 @@ var GeneralTab = React.createClass({ if (this.props.activeSection === 'username') { inputs.push( <div className='form-group'> - <label className='col-sm-5 control-label'>{utils.isMobile() ? '': 'Username'}</label> + <label className='col-sm-5 control-label'>{utils.isMobile() ? '' : 'Username'}</label> <div className='col-sm-7'> <input className='form-control' type='text' onChange={this.updateUsername} value={this.state.username}/> </div> @@ -925,8 +948,8 @@ var GeneralTab = React.createClass({ title='Username' inputs={inputs} submit={this.submitUsername} - server_error={server_error} - client_error={client_error} + server_error={serverError} + client_error={clientError} updateSection={function(e) { self.updateSection(''); e.preventDefault(); @@ -960,8 +983,8 @@ var GeneralTab = React.createClass({ title='Email' inputs={inputs} submit={this.submitEmail} - server_error={server_error} - client_error={email_error} + server_error={serverError} + client_error={emailError} updateSection={function(e) { self.updateSection(''); e.preventDefault(); @@ -987,8 +1010,8 @@ var GeneralTab = React.createClass({ title='Profile Picture' submit={this.submitPicture} src={'/api/v1/users/' + user.id + '/image?time=' + user.last_picture_update} - server_error={server_error} - client_error={client_error} + server_error={serverError} + client_error={clientError} updateSection={function(e) { self.updateSection(''); e.preventDefault(); @@ -996,6 +1019,7 @@ var GeneralTab = React.createClass({ picture={this.state.picture} pictureChange={this.updatePicture} submitActive={this.submitActive} + loadingPicture={this.state.loadingPicture} /> ); } else { @@ -1063,6 +1087,7 @@ var AppearanceTab = React.createClass({ }, handleClose: function() { this.setState({server_error: null}); + this.props.updateTab('general'); }, componentDidMount: function() { if (this.props.activeSection === "theme") { @@ -1078,6 +1103,7 @@ var AppearanceTab = React.createClass({ }, componentWillUnmount: function() { $('#user_settings1').off('hidden.bs.modal', this.handleClose); + this.props.updateSection(''); }, getInitialState: function() { var user = UserStore.getCurrentUser(); @@ -1146,10 +1172,11 @@ var AppearanceTab = React.createClass({ </div> </div> ); - } + } }); module.exports = React.createClass({ + displayName: 'UserSettings', componentDidMount: function() { UserStore.addChangeListener(this._onChange); }, @@ -1175,19 +1202,19 @@ module.exports = React.createClass({ } else if (this.props.activeTab === 'security') { return ( <div> - <SecurityTab user={this.state.user} activeSection={this.props.activeSection} updateSection={this.props.updateSection} /> + <SecurityTab user={this.state.user} activeSection={this.props.activeSection} updateSection={this.props.updateSection} updateTab={this.props.updateTab} /> </div> ); } else if (this.props.activeTab === 'notifications') { return ( <div> - <NotificationsTab user={this.state.user} activeSection={this.props.activeSection} updateSection={this.props.updateSection} /> + <NotificationsTab user={this.state.user} activeSection={this.props.activeSection} updateSection={this.props.updateSection} updateTab={this.props.updateTab} /> </div> ); } else if (this.props.activeTab === 'appearance') { return ( <div> - <AppearanceTab activeSection={this.props.activeSection} updateSection={this.props.updateSection} /> + <AppearanceTab activeSection={this.props.activeSection} updateSection={this.props.updateSection} updateTab={this.props.updateTab} /> </div> ); } else { diff --git a/web/react/components/user_settings_modal.jsx b/web/react/components/user_settings_modal.jsx index 421027244..702e7ad7a 100644 --- a/web/react/components/user_settings_modal.jsx +++ b/web/react/components/user_settings_modal.jsx @@ -32,7 +32,7 @@ module.exports = React.createClass({ tabs.push({name: "appearance", ui_name: "Appearance", icon: "glyphicon glyphicon-wrench"}); return ( - <div className="modal fade" ref="modal" id="user_settings1" role="dialog" aria-hidden="true"> + <div className="modal fade" ref="modal" id="user_settings1" role="dialog" tabIndex="-1" aria-hidden="true"> <div className="modal-dialog settings-modal"> <div className="modal-content"> <div className="modal-header"> @@ -53,6 +53,7 @@ module.exports = React.createClass({ activeTab={this.state.active_tab} activeSection={this.state.active_section} updateSection={this.updateSection} + updateTab={this.updateTab} /> </div> </div> diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx index ecf54ede6..ea1e75ecb 100644 --- a/web/react/stores/post_store.jsx +++ b/web/react/stores/post_store.jsx @@ -6,7 +6,6 @@ var EventEmitter = require('events').EventEmitter; var assign = require('object-assign'); var ChannelStore = require('../stores/channel_store.jsx'); -var UserStore = require('../stores/user_store.jsx'); var BrowserStore = require('../stores/browser_store.jsx'); var Constants = require('../utils/constants.jsx'); @@ -21,185 +20,184 @@ var ADD_MENTION_EVENT = 'add_mention'; var PostStore = assign({}, EventEmitter.prototype, { - emitChange: function() { - this.emit(CHANGE_EVENT); - }, - - addChangeListener: function(callback) { - this.on(CHANGE_EVENT, callback); - }, - - removeChangeListener: function(callback) { - this.removeListener(CHANGE_EVENT, callback); - }, - - emitSearchChange: function() { - this.emit(SEARCH_CHANGE_EVENT); - }, - - addSearchChangeListener: function(callback) { - this.on(SEARCH_CHANGE_EVENT, callback); - }, - - removeSearchChangeListener: function(callback) { - this.removeListener(SEARCH_CHANGE_EVENT, callback); - }, - - emitSearchTermChange: function(doSearch, isMentionSearch) { - this.emit(SEARCH_TERM_CHANGE_EVENT, doSearch, isMentionSearch); - }, - - addSearchTermChangeListener: function(callback) { - this.on(SEARCH_TERM_CHANGE_EVENT, callback); - }, - - removeSearchTermChangeListener: function(callback) { - this.removeListener(SEARCH_TERM_CHANGE_EVENT, callback); - }, - - emitSelectedPostChange: function(from_search) { - this.emit(SELECTED_POST_CHANGE_EVENT, from_search); - }, - - addSelectedPostChangeListener: function(callback) { - this.on(SELECTED_POST_CHANGE_EVENT, callback); - }, - - removeSelectedPostChangeListener: function(callback) { - this.removeListener(SELECTED_POST_CHANGE_EVENT, callback); - }, - - emitMentionDataChange: function(id, mentionText, excludeList) { - this.emit(MENTION_DATA_CHANGE_EVENT, id, mentionText, excludeList); - }, - - addMentionDataChangeListener: function(callback) { - this.on(MENTION_DATA_CHANGE_EVENT, callback); - }, - - removeMentionDataChangeListener: function(callback) { - this.removeListener(MENTION_DATA_CHANGE_EVENT, callback); - }, - - emitAddMention: function(id, username) { - this.emit(ADD_MENTION_EVENT, id, username); - }, - - addAddMentionListener: function(callback) { - this.on(ADD_MENTION_EVENT, callback); - }, - - removeAddMentionListener: function(callback) { - this.removeListener(ADD_MENTION_EVENT, callback); - }, - - getCurrentPosts: function() { - var currentId = ChannelStore.getCurrentId(); - - if (currentId != null) - return this.getPosts(currentId); - else - return null; - }, - storePosts: function(channelId, posts) { - this._storePosts(channelId, posts); - this.emitChange(); - }, - _storePosts: function(channelId, posts) { - BrowserStore.setItem("posts_" + channelId, posts); - }, - getPosts: function(channelId) { - return BrowserStore.getItem("posts_" + channelId); - }, - storeSearchResults: function(results, is_mention_search) { - BrowserStore.setItem("search_results", results); - is_mention_search = is_mention_search ? true : false; // force to bool - BrowserStore.setItem("is_mention_search", is_mention_search); - }, - getSearchResults: function() { - return BrowserStore.getItem("search_results"); - }, - getIsMentionSearch: function() { - return BrowserStore.getItem("is_mention_search"); - }, - storeSelectedPost: function(post_list) { - BrowserStore.setItem("select_post", post_list); - }, - getSelectedPost: function() { - return BrowserStore.getItem("select_post"); - }, - storeSearchTerm: function(term) { - BrowserStore.setItem("search_term", term); - }, - getSearchTerm: function() { - return BrowserStore.getItem("search_term"); - }, - storeCurrentDraft: function(draft) { - var channel_id = ChannelStore.getCurrentId(); - BrowserStore.setItem("draft_" + channel_id, draft); - }, - getCurrentDraft: function() { - var channel_id = ChannelStore.getCurrentId(); - return BrowserStore.getItem("draft_" + channel_id); - }, - storeDraft: function(channel_id, draft) { - BrowserStore.setItem("draft_" + channel_id, draft); - }, - getDraft: function(channel_id) { - return BrowserStore.getItem("draft_" + channel_id); - }, - storeCommentDraft: function(parent_post_id, draft) { - BrowserStore.setItem("comment_draft_" + parent_post_id, draft); - }, - getCommentDraft: function(parent_post_id) { - return BrowserStore.getItem("comment_draft_" + parent_post_id); - }, - clearDraftUploads: function() { - BrowserStore.actionOnItemsWithPrefix("draft_", function (key, value) { - if (value) { - value.uploadsInProgress = 0; - BrowserStore.setItem(key, value); - } - }); - }, - clearCommentDraftUploads: function() { - BrowserStore.actionOnItemsWithPrefix("comment_draft_", function (key, value) { - if (value) { - value.uploadsInProgress = 0; - BrowserStore.setItem(key, value); - } - }); - } + emitChange: function emitChange() { + this.emit(CHANGE_EVENT); + }, + + addChangeListener: function addChangeListener(callback) { + this.on(CHANGE_EVENT, callback); + }, + + removeChangeListener: function removeChangeListener(callback) { + this.removeListener(CHANGE_EVENT, callback); + }, + + emitSearchChange: function emitSearchChange() { + this.emit(SEARCH_CHANGE_EVENT); + }, + + addSearchChangeListener: function addSearchChangeListener(callback) { + this.on(SEARCH_CHANGE_EVENT, callback); + }, + + removeSearchChangeListener: function removeSearchChangeListener(callback) { + this.removeListener(SEARCH_CHANGE_EVENT, callback); + }, + + emitSearchTermChange: function emitSearchTermChange(doSearch, isMentionSearch) { + this.emit(SEARCH_TERM_CHANGE_EVENT, doSearch, isMentionSearch); + }, + + addSearchTermChangeListener: function addSearchTermChangeListener(callback) { + this.on(SEARCH_TERM_CHANGE_EVENT, callback); + }, + + removeSearchTermChangeListener: function removeSearchTermChangeListener(callback) { + this.removeListener(SEARCH_TERM_CHANGE_EVENT, callback); + }, + + emitSelectedPostChange: function emitSelectedPostChange(fromSearch) { + this.emit(SELECTED_POST_CHANGE_EVENT, fromSearch); + }, + + addSelectedPostChangeListener: function addSelectedPostChangeListener(callback) { + this.on(SELECTED_POST_CHANGE_EVENT, callback); + }, + + removeSelectedPostChangeListener: function removeSelectedPostChangeListener(callback) { + this.removeListener(SELECTED_POST_CHANGE_EVENT, callback); + }, + + emitMentionDataChange: function emitMentionDataChange(id, mentionText) { + this.emit(MENTION_DATA_CHANGE_EVENT, id, mentionText); + }, + + addMentionDataChangeListener: function addMentionDataChangeListener(callback) { + this.on(MENTION_DATA_CHANGE_EVENT, callback); + }, + + removeMentionDataChangeListener: function removeMentionDataChangeListener(callback) { + this.removeListener(MENTION_DATA_CHANGE_EVENT, callback); + }, + + emitAddMention: function emitAddMention(id, username) { + this.emit(ADD_MENTION_EVENT, id, username); + }, + + addAddMentionListener: function addAddMentionListener(callback) { + this.on(ADD_MENTION_EVENT, callback); + }, + + removeAddMentionListener: function removeAddMentionListener(callback) { + this.removeListener(ADD_MENTION_EVENT, callback); + }, + + getCurrentPosts: function getCurrentPosts() { + var currentId = ChannelStore.getCurrentId(); + + if (currentId != null) { + return this.getPosts(currentId); + } + return null; + }, + storePosts: function storePosts(channelId, posts) { + this.pStorePosts(channelId, posts); + this.emitChange(); + }, + pStorePosts: function pStorePosts(channelId, posts) { + BrowserStore.setItem('posts_' + channelId, posts); + }, + getPosts: function getPosts(channelId) { + return BrowserStore.getItem('posts_' + channelId); + }, + storeSearchResults: function storeSearchResults(results, isMentionSearch) { + BrowserStore.setItem('search_results', results); + BrowserStore.setItem('is_mention_search', Boolean(isMentionSearch)); + }, + getSearchResults: function getSearchResults() { + return BrowserStore.getItem('search_results'); + }, + getIsMentionSearch: function getIsMentionSearch() { + return BrowserStore.getItem('is_mention_search'); + }, + storeSelectedPost: function storeSelectedPost(postList) { + BrowserStore.setItem('select_post', postList); + }, + getSelectedPost: function getSelectedPost() { + return BrowserStore.getItem('select_post'); + }, + storeSearchTerm: function storeSearchTerm(term) { + BrowserStore.setItem('search_term', term); + }, + getSearchTerm: function getSearchTerm() { + return BrowserStore.getItem('search_term'); + }, + storeCurrentDraft: function storeCurrentDraft(draft) { + var channelId = ChannelStore.getCurrentId(); + BrowserStore.setItem('draft_' + channelId, draft); + }, + getCurrentDraft: function getCurrentDraft() { + var channelId = ChannelStore.getCurrentId(); + return BrowserStore.getItem('draft_' + channelId); + }, + storeDraft: function storeDraft(channelId, draft) { + BrowserStore.setItem('draft_' + channelId, draft); + }, + getDraft: function getDraft(channelId) { + return BrowserStore.getItem('draft_' + channelId); + }, + storeCommentDraft: function storeCommentDraft(parentPostId, draft) { + BrowserStore.setItem('comment_draft_' + parentPostId, draft); + }, + getCommentDraft: function getCommentDraft(parentPostId) { + return BrowserStore.getItem('comment_draft_' + parentPostId); + }, + clearDraftUploads: function clearDraftUploads() { + BrowserStore.actionOnItemsWithPrefix('draft_', function clearUploads(key, value) { + if (value) { + value.uploadsInProgress = 0; + BrowserStore.setItem(key, value); + } + }); + }, + clearCommentDraftUploads: function clearCommentDraftUploads() { + BrowserStore.actionOnItemsWithPrefix('comment_draft_', function clearUploads(key, value) { + if (value) { + value.uploadsInProgress = 0; + BrowserStore.setItem(key, value); + } + }); + } }); -PostStore.dispatchToken = AppDispatcher.register(function(payload) { - var action = payload.action; - - switch(action.type) { - case ActionTypes.RECIEVED_POSTS: - PostStore._storePosts(action.id, action.post_list); - PostStore.emitChange(); - break; - case ActionTypes.RECIEVED_SEARCH: - PostStore.storeSearchResults(action.results, action.is_mention_search); - PostStore.emitSearchChange(); - break; - case ActionTypes.RECIEVED_SEARCH_TERM: - PostStore.storeSearchTerm(action.term); - PostStore.emitSearchTermChange(action.do_search, action.is_mention_search); - break; - case ActionTypes.RECIEVED_POST_SELECTED: - PostStore.storeSelectedPost(action.post_list); - PostStore.emitSelectedPostChange(action.from_search); - break; - case ActionTypes.RECIEVED_MENTION_DATA: - PostStore.emitMentionDataChange(action.id, action.mention_text, action.exclude_list); - break; - case ActionTypes.RECIEVED_ADD_MENTION: - PostStore.emitAddMention(action.id, action.username); - break; - default: - } +PostStore.dispatchToken = AppDispatcher.register(function registry(payload) { + var action = payload.action; + + switch (action.type) { + case ActionTypes.RECIEVED_POSTS: + PostStore.pStorePosts(action.id, action.post_list); + PostStore.emitChange(); + break; + case ActionTypes.RECIEVED_SEARCH: + PostStore.storeSearchResults(action.results, action.is_mention_search); + PostStore.emitSearchChange(); + break; + case ActionTypes.RECIEVED_SEARCH_TERM: + PostStore.storeSearchTerm(action.term); + PostStore.emitSearchTermChange(action.do_search, action.is_mention_search); + break; + case ActionTypes.RECIEVED_POST_SELECTED: + PostStore.storeSelectedPost(action.post_list); + PostStore.emitSelectedPostChange(action.from_search); + break; + case ActionTypes.RECIEVED_MENTION_DATA: + PostStore.emitMentionDataChange(action.id, action.mention_text); + break; + case ActionTypes.RECIEVED_ADD_MENTION: + PostStore.emitAddMention(action.id, action.username); + break; + default: + } }); module.exports = PostStore; diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 2b0976afd..bed0ec556 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -82,7 +82,7 @@ module.exports = { "channel", ], MONTHS: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], - MAX_DMS: 10, + MAX_DMS: 20, ONLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path class='online--icon' d='M6,5.487c1.371,0,2.482-1.116,2.482-2.493c0-1.378-1.111-2.495-2.482-2.495S3.518,1.616,3.518,2.994C3.518,4.371,4.629,5.487,6,5.487z M10.452,8.545c-0.101-0.829-0.36-1.968-0.726-2.541C9.475,5.606,8.5,5.5,8.5,5.5S8.43,7.521,6,7.521C3.507,7.521,3.5,5.5,3.5,5.5S2.527,5.606,2.273,6.004C1.908,6.577,1.648,7.716,1.547,8.545C1.521,8.688,1.49,9.082,1.498,9.142c0.161,1.295,2.238,2.322,4.375,2.358C5.916,11.501,5.958,11.501,6,11.501c0.043,0,0.084,0,0.127-0.001c2.076-0.026,4.214-1.063,4.375-2.358C10.509,9.082,10.471,8.696,10.452,8.545z'/></g></g></svg>", OFFLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path fill='#cccccc' d='M6.002,7.143C5.645,7.363,5.167,7.52,4.502,7.52c-2.493,0-2.5-2.02-2.5-2.02S1.029,5.607,0.775,6.004C0.41,6.577,0.15,7.716,0.049,8.545c-0.025,0.145-0.057,0.537-0.05,0.598c0.162,1.295,2.237,2.321,4.375,2.357c0.043,0.001,0.085,0.001,0.127,0.001c0.043,0,0.084,0,0.127-0.001c1.879-0.023,3.793-0.879,4.263-2h-2.89L6.002,7.143L6.002,7.143z M4.501,5.488c1.372,0,2.483-1.117,2.483-2.494c0-1.378-1.111-2.495-2.483-2.495c-1.371,0-2.481,1.117-2.481,2.495C2.02,4.371,3.13,5.488,4.501,5.488z M7.002,6.5v2h5v-2H7.002z'/></g></g></svg>", MENU_ICON: "<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>", diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss index 52659521d..78006ff18 100644 --- a/web/sass-files/sass/partials/_base.scss +++ b/web/sass-files/sass/partials/_base.scss @@ -49,6 +49,12 @@ div.theme { background-color: $primary-color; } +.tooltip { + .tooltip-inner { + word-break: break-word; + } +} + .nopadding { padding: 0; margin: 0; @@ -61,6 +67,10 @@ div.theme { } } +.form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control { + cursor: auto; +} + .form-group { &.form-group--small { margin-bottom: 10px; diff --git a/web/sass-files/sass/partials/_files.scss b/web/sass-files/sass/partials/_files.scss index 22e2f44c5..65775f01e 100644 --- a/web/sass-files/sass/partials/_files.scss +++ b/web/sass-files/sass/partials/_files.scss @@ -4,7 +4,8 @@ max-height: 110px; height: 110px; white-space: nowrap; - overflow: auto; + overflow-x: auto; + overflow-y: hidden; .preview-div { display: inline-block; width: 120px; @@ -28,9 +29,9 @@ } } .preview-img { - display: block; - height: auto; - max-width: 100%; + display: block; + height: auto; + max-width: 100%; } .remove-preview { position: absolute; @@ -129,10 +130,10 @@ background-color: #FFF; background-repeat: no-repeat; &.small { - background-position: center; + background-position: center; } &.normal { - background-position: top left; + background-position: top left; } } .post-image__thumbnail { @@ -140,6 +141,8 @@ vertical-align: top; width: 50%; height: 100%; + cursor: zoom-in; + cursor: -webkit-zoom-in; } .post-image__details { display: table-cell; @@ -168,34 +171,34 @@ } .file-details__container { - @include display-flex; - display: -ms-flexbox; + @include display-flex; + display: -ms-flexbox; - .file-details { - width: 320px; - height: 270px; - padding: 14px; - text-align: left; - vertical-align: top; + .file-details { + width: 320px; + height: 270px; + padding: 14px; + text-align: left; + vertical-align: top; - .file-details__name { - font-size: 16px; - } - .file-details__info { - color: grey; - } + .file-details__name { + font-size: 16px; } - .file-details__preview { - width: 320px; - height: 270px; - border-right: 1px solid #ddd; - vertical-align: center; + .file-details__info { + color: grey; + } + } + .file-details__preview { + width: 320px; + height: 270px; + border-right: 1px solid #ddd; + vertical-align: center; // helper to center the image icon in the preview window .file-details__preview-helper { - height: 100%; - display: inline-block; - vertical-align: middle; + height: 100%; + display: inline-block; + vertical-align: middle; } + } } -} diff --git a/web/sass-files/sass/partials/_get-link.scss b/web/sass-files/sass/partials/_get-link.scss new file mode 100644 index 000000000..c84befd6a --- /dev/null +++ b/web/sass-files/sass/partials/_get-link.scss @@ -0,0 +1,6 @@ +.copy-link-confirm { + position: fixed; + color: rgb(153, 230, 153); + top: 84%; + left: 130px; +}
\ No newline at end of file diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss index fb37c43eb..da648a170 100644 --- a/web/sass-files/sass/partials/_headers.scss +++ b/web/sass-files/sass/partials/_headers.scss @@ -110,6 +110,20 @@ } } } + &.theme--black { + &:hover { + &:before { + background: rgba(white, 0.2); + } + } + } + &.theme--gray { + &:hover { + &:before { + background: rgba(white, 0.1); + } + } + } a { color: #fff; } diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss index f359037c5..014f834ed 100644 --- a/web/sass-files/sass/partials/_modal.scss +++ b/web/sass-files/sass/partials/_modal.scss @@ -15,10 +15,13 @@ } .remove__member { float: right; - color: #E56565; + color: #999; font-size: 20px; line-height: 0; padding: 6px; + &:hover { + color: #E56565; + } } .modal-dialog { max-width: 95%; @@ -151,10 +154,9 @@ height: 100%; margin: 0 auto; .image-wrapper { - background: #FFF; position: relative; max-width: 90%; - min-height: 50px; + min-height: 100px; min-width: 320px; @include border-radius(3px); display: table; @@ -182,6 +184,7 @@ z-index: 9999; } > a { + background: #FFF; display: table-cell; vertical-align: middle; } diff --git a/web/sass-files/sass/partials/_navbar.scss b/web/sass-files/sass/partials/_navbar.scss index 905907d84..2e78a8728 100644 --- a/web/sass-files/sass/partials/_navbar.scss +++ b/web/sass-files/sass/partials/_navbar.scss @@ -19,6 +19,7 @@ } } .navbar-toggle { + width: 43px; float: left; border-color: transparent; border-radius: 0; diff --git a/web/sass-files/sass/partials/_popover.scss b/web/sass-files/sass/partials/_popover.scss index fa1b44841..5008331b4 100644 --- a/web/sass-files/sass/partials/_popover.scss +++ b/web/sass-files/sass/partials/_popover.scss @@ -6,4 +6,11 @@ .user-popover__image { margin: 0 0 10px; @include border-radius(128px); +} + +.user-popover__email { + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + display: block; }
\ No newline at end of file diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index e3f140413..47b2b6bd7 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -229,6 +229,16 @@ } } +@media screen and (max-height: 640px) { + .signup-team__container { + padding: 30px 0; + margin-bottom: 30px; + font-size: 0.9em; + .signup-team__name { + font-size: 2em; + } + } +} @media screen and (max-width: 768px) { .date-separator, .new-separator { &.hovered--after { diff --git a/web/sass-files/sass/styles.scss b/web/sass-files/sass/styles.scss index ffd1f42b8..eb5152a2c 100644 --- a/web/sass-files/sass/styles.scss +++ b/web/sass-files/sass/styles.scss @@ -33,6 +33,7 @@ @import "partials/error"; @import "partials/error-bar"; @import "partials/loading"; +@import "partials/get-link"; // Responsive Css @import "partials/responsive"; diff --git a/web/static/config/config.js b/web/static/config/config.js index 0d564b77e..00cae7ab2 100644 --- a/web/static/config/config.js +++ b/web/static/config/config.js @@ -31,6 +31,9 @@ var config = { ReportProblemLink: "/static/help/configure_links.html", HomeLink: "", + // Toggle whether or not users are shown a message about agreeing to the Terms of Service during the signup process + ShowTermsDuringSignup: false, + ThemeColors: ["#2389d7", "#008a17", "#dc4fad", "#ac193d", "#0072c6", "#d24726", "#ff8f32", "#82ba00", "#03b3b2", "#008299", "#4617b4", "#8c0095", "#004b8b", "#004b8b", "#570000", "#380000", "#585858", "#000000"] }; diff --git a/web/templates/channel.html b/web/templates/channel.html index 6325069ee..da6fed97d 100644 --- a/web/templates/channel.html +++ b/web/templates/channel.html @@ -49,7 +49,9 @@ <div id="activity_log_modal"></div> <div id="removed_from_channel_modal"></div> <script> -window.setup_channel_page('{{ .Props.TeamDisplayName }}', '{{ .Props.TeamType }}', '{{ .Props.TeamId }}', '{{ .Props.ChannelName }}', '{{ .Props.ChannelId }}'); + window.setup_channel_page('{{ .Props.TeamDisplayName }}', '{{ .Props.TeamType }}', '{{ .Props.TeamId }}', '{{ .Props.ChannelName }}', '{{ .Props.ChannelId }}'); + $('body').tooltip( {selector: '[data-toggle=tooltip]'} ); + $('.modal-body').perfectScrollbar(); </script> </body> </html> |