diff options
Diffstat (limited to 'web')
-rw-r--r-- | web/react/components/access_history_modal.jsx | 108 | ||||
-rw-r--r-- | web/react/components/channel_notifications.jsx | 152 | ||||
-rw-r--r-- | web/react/components/get_link_modal.jsx | 141 | ||||
-rw-r--r-- | web/react/components/loading_screen.jsx | 37 | ||||
-rw-r--r-- | web/react/components/navbar.jsx | 436 | ||||
-rw-r--r-- | web/react/components/post_header.jsx | 41 | ||||
-rw-r--r-- | web/react/components/sidebar.jsx | 574 | ||||
-rw-r--r-- | web/react/components/team_feature_tab.jsx | 116 | ||||
-rw-r--r-- | web/react/components/team_settings.jsx | 121 | ||||
-rw-r--r-- | web/react/components/team_signup_allowed_domains_page.jsx | 96 | ||||
-rw-r--r-- | web/react/components/team_signup_password_page.jsx | 91 | ||||
-rw-r--r-- | web/react/components/user_profile.jsx | 104 | ||||
-rw-r--r-- | web/react/components/user_settings_appearance.jsx | 168 | ||||
-rw-r--r-- | web/react/components/user_settings_security.jsx | 180 | ||||
-rw-r--r-- | web/react/components/view_image.jsx | 292 |
15 files changed, 1678 insertions, 979 deletions
diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx index a19e5c16e..9c8e7c6c3 100644 --- a/web/react/components/access_history_modal.jsx +++ b/web/react/components/access_history_modal.jsx @@ -4,48 +4,49 @@ var UserStore = require('../stores/user_store.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var LoadingScreen = require('./loading_screen.jsx'); -var utils = require('../utils/utils.jsx'); +var Utils = require('../utils/utils.jsx'); -function getStateFromStoresForAudits() { - return { - audits: UserStore.getAudits() - }; -} +export default class AccessHistoryModal extends React.Component { + constructor(props) { + super(props); + + this.onAuditChange = this.onAuditChange.bind(this); + this.handleMoreInfo = this.handleMoreInfo.bind(this); -module.exports = React.createClass({ - displayName: 'AccessHistoryModal', - componentDidMount: function() { - UserStore.addAuditsChangeListener(this.onListenerChange); - $(this.refs.modal.getDOMNode()).on('shown.bs.modal', function() { + this.state = this.getStateFromStoresForAudits(); + this.state.moreInfo = []; + } + getStateFromStoresForAudits() { + return { + audits: UserStore.getAudits() + }; + } + componentDidMount() { + UserStore.addAuditsChangeListener(this.onAuditChange); + $(React.findDOMNode(this.refs.modal)).on('shown.bs.modal', function show() { AsyncClient.getAudits(); }); - var self = this; - $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', function() { + $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', function hide() { $('#user_settings').modal('show'); - self.setState({moreInfo: []}); - }); - }, - componentWillUnmount: function() { - UserStore.removeAuditsChangeListener(this.onListenerChange); - }, - onListenerChange: function() { - var newState = getStateFromStoresForAudits(); - if (!utils.areStatesEqual(newState.audits, this.state.audits)) { + this.setState({moreInfo: []}); + }.bind(this)); + } + componentWillUnmount() { + UserStore.removeAuditsChangeListener(this.onAuditChange); + } + onAuditChange() { + var newState = this.getStateFromStoresForAudits(); + if (!Utils.areStatesEqual(newState.audits, this.state.audits)) { this.setState(newState); } - }, - handleMoreInfo: function(index) { + } + handleMoreInfo(index) { var newMoreInfo = this.state.moreInfo; newMoreInfo[index] = true; this.setState({moreInfo: newMoreInfo}); - }, - getInitialState: function() { - var initialState = getStateFromStoresForAudits(); - initialState.moreInfo = []; - return initialState; - }, - render: function() { + } + render() { var accessList = []; var currentHistoryDate = null; @@ -63,7 +64,16 @@ module.exports = React.createClass({ currentAudit.session_id = 'N/A (Login attempt)'; } - var moreInfo = (<a href='#' className='theme' onClick={this.handleMoreInfo.bind(this, i)}>More info</a>); + var moreInfo = ( + <a + href='#' + className='theme' + onClick={this.handleMoreInfo.bind(this, i)} + > + More info + </a> + ); + if (this.state.moreInfo[i]) { moreInfo = ( <div> @@ -75,7 +85,7 @@ module.exports = React.createClass({ var divider = null; if (i < this.state.audits.length - 1) { - divider = (<div className='divider-light'></div>) + divider = (<div className='divider-light'></div>); } accessList[i] = ( @@ -102,14 +112,36 @@ module.exports = React.createClass({ return ( <div> - <div className='modal fade' ref='modal' id='access-history' tabIndex='-1' role='dialog' aria-hidden='true'> + <div + className='modal fade' + ref='modal' + id='access-history' + tabIndex='-1' + role='dialog' + aria-hidden='true' + > <div className='modal-dialog modal-lg'> <div className='modal-content'> <div className='modal-header'> - <button type='button' className='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>×</span></button> - <h4 className='modal-title' id='myModalLabel'>Access History</h4> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>×</span> + </button> + <h4 + className='modal-title' + id='myModalLabel' + > + Access History + </h4> </div> - <div ref='modalBody' className='modal-body'> + <div + ref='modalBody' + className='modal-body' + > {content} </div> </div> @@ -118,4 +150,4 @@ module.exports = React.createClass({ </div> ); } -}); +} diff --git a/web/react/components/channel_notifications.jsx b/web/react/components/channel_notifications.jsx index 884bad9d2..173646597 100644 --- a/web/react/components/channel_notifications.jsx +++ b/web/react/components/channel_notifications.jsx @@ -4,26 +4,29 @@ var SettingItemMin = require('./setting_item_min.jsx'); var SettingItemMax = require('./setting_item_max.jsx'); -var utils = require('../utils/utils.jsx'); -var client = require('../utils/client.jsx'); +var Utils = require('../utils/utils.jsx'); +var Client = require('../utils/client.jsx'); var UserStore = require('../stores/user_store.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); export default class ChannelNotifications extends React.Component { constructor(props) { super(props); + this.onListenerChange = this.onListenerChange.bind(this); this.updateSection = this.updateSection.bind(this); this.handleUpdate = this.handleUpdate.bind(this); this.handleRadioClick = this.handleRadioClick.bind(this); this.handleQuietToggle = this.handleQuietToggle.bind(this); + this.createDesktopSection = this.createDesktopSection.bind(this); + this.createQuietSection = this.createQuietSection.bind(this); + this.state = {notifyLevel: '', title: '', channelId: '', activeSection: ''}; } componentDidMount() { ChannelStore.addChangeListener(this.onListenerChange); - var self = this; - $(this.refs.modal.getDOMNode()).on('show.bs.modal', function showModal(e) { + $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function showModal(e) { var button = e.relatedTarget; var channelId = button.getAttribute('data-channelid'); @@ -34,8 +37,8 @@ export default class ChannelNotifications extends React.Component { quietMode = true; } - self.setState({notifyLevel: notifyLevel, quietMode: quietMode, title: button.getAttribute('data-title'), channelId: channelId}); - }); + this.setState({notifyLevel: notifyLevel, quietMode: quietMode, title: button.getAttribute('data-title'), channelId: channelId}); + }.bind(this)); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onListenerChange); @@ -55,7 +58,7 @@ export default class ChannelNotifications extends React.Component { newState.notifyLevel = notifyLevel; newState.quietMode = quietMode; - if (!utils.areStatesEqual(this.state, newState)) { + if (!Utils.areStatesEqual(this.state, newState)) { this.setState(newState); } } @@ -78,7 +81,7 @@ export default class ChannelNotifications extends React.Component { return; } - client.updateNotifyLevel(data, + Client.updateNotifyLevel(data, function success() { var member = ChannelStore.getMember(channelId); member.notify_level = notifyLevel; @@ -92,25 +95,15 @@ export default class ChannelNotifications extends React.Component { } handleRadioClick(notifyLevel) { this.setState({notifyLevel: notifyLevel, quietMode: false}); - this.refs.modal.getDOMNode().focus(); + React.findDOMNode(this.refs.modal).focus(); } handleQuietToggle(quietMode) { this.setState({notifyLevel: 'none', quietMode: quietMode}); - this.refs.modal.getDOMNode().focus(); + React.findDOMNode(this.refs.modal).focus(); } - render() { - var serverError = null; - if (this.state.serverError) { - serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>; - } - - var self = this; - var describe = ''; - var inputs = []; - + createDesktopSection(serverError) { var handleUpdateSection; - var desktopSection; if (this.state.activeSection === 'desktop') { var notifyActive = [false, false, false]; if (this.state.notifyLevel === 'mention') { @@ -121,6 +114,8 @@ export default class ChannelNotifications extends React.Component { notifyActive[2] = true; } + var inputs = []; + inputs.push( <div> <div className='radio'> @@ -128,7 +123,7 @@ export default class ChannelNotifications extends React.Component { <input type='radio' checked={notifyActive[0]} - onChange={self.handleRadioClick.bind(this, 'all')} + onChange={this.handleRadioClick.bind(this, 'all')} > For all activity </input> @@ -140,7 +135,7 @@ export default class ChannelNotifications extends React.Component { <input type='radio' checked={notifyActive[1]} - onChange={self.handleRadioClick.bind(this, 'mention')} + onChange={this.handleRadioClick.bind(this, 'mention')} > Only for mentions </input> @@ -152,7 +147,7 @@ export default class ChannelNotifications extends React.Component { <input type='radio' checked={notifyActive[2]} - onChange={self.handleRadioClick.bind(this, 'none')} + onChange={this.handleRadioClick.bind(this, 'none')} > Never </input> @@ -162,12 +157,12 @@ export default class ChannelNotifications extends React.Component { ); handleUpdateSection = function updateSection(e) { - self.updateSection(''); - self.onListenerChange(); + this.updateSection(''); + this.onListenerChange(); e.preventDefault(); - }; + }.bind(this); - desktopSection = ( + return ( <SettingItemMax title='Send desktop notifications' inputs={inputs} @@ -176,30 +171,32 @@ export default class ChannelNotifications extends React.Component { updateSection={handleUpdateSection} /> ); - } else { - if (this.state.notifyLevel === 'mention') { - describe = 'Only for mentions'; - } else if (this.state.notifyLevel === 'all') { - describe = 'For all activity'; - } else { - describe = 'Never'; - } - - handleUpdateSection = function updateSection(e) { - self.updateSection('desktop'); - e.preventDefault(); - }; + } - desktopSection = ( - <SettingItemMin - title='Send desktop notifications' - describe={describe} - updateSection={handleUpdateSection} - /> - ); + var describe; + if (this.state.notifyLevel === 'mention') { + describe = 'Only for mentions'; + } else if (this.state.notifyLevel === 'all') { + describe = 'For all activity'; + } else { + describe = 'Never'; } - var quietSection; + handleUpdateSection = function updateSection(e) { + this.updateSection('desktop'); + e.preventDefault(); + }.bind(this); + + return ( + <SettingItemMin + title='Send desktop notifications' + describe={describe} + updateSection={handleUpdateSection} + /> + ); + } + createQuietSection(serverError) { + var handleUpdateSection; if (this.state.activeSection === 'quiet') { var quietActive = [false, false]; if (this.state.quietMode) { @@ -208,6 +205,8 @@ export default class ChannelNotifications extends React.Component { quietActive[1] = true; } + var inputs = []; + inputs.push( <div> <div className='radio'> @@ -215,7 +214,7 @@ export default class ChannelNotifications extends React.Component { <input type='radio' checked={quietActive[0]} - onChange={self.handleQuietToggle.bind(this, true)} + onChange={this.handleQuietToggle.bind(this, true)} > On </input> @@ -227,7 +226,7 @@ export default class ChannelNotifications extends React.Component { <input type='radio' checked={quietActive[1]} - onChange={self.handleQuietToggle.bind(this, false)} + onChange={this.handleQuietToggle.bind(this, false)} > Off </input> @@ -245,12 +244,12 @@ export default class ChannelNotifications extends React.Component { ); handleUpdateSection = function updateSection(e) { - self.updateSection(''); - self.onListenerChange(); + this.updateSection(''); + this.onListenerChange(); e.preventDefault(); - }; + }.bind(this); - quietSection = ( + return ( <SettingItemMax title='Quiet mode' inputs={inputs} @@ -259,27 +258,38 @@ export default class ChannelNotifications extends React.Component { updateSection={handleUpdateSection} /> ); + } + + var describe; + if (this.state.quietMode) { + describe = 'On'; } else { - if (this.state.quietMode) { - describe = 'On'; - } else { - describe = 'Off'; - } + describe = 'Off'; + } - handleUpdateSection = function updateSection(e) { - self.updateSection('quiet'); - e.preventDefault(); - }; + handleUpdateSection = function updateSection(e) { + this.updateSection('quiet'); + e.preventDefault(); + }.bind(this); - quietSection = ( - <SettingItemMin - title='Quiet mode' - describe={describe} - updateSection={handleUpdateSection} - /> - ); + return ( + <SettingItemMin + title='Quiet mode' + describe={describe} + updateSection={handleUpdateSection} + /> + ); + } + render() { + var serverError = null; + if (this.state.serverError) { + serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>; } + var desktopSection = this.createDesktopSection(serverError); + + var quietSection = this.createQuietSection(serverError); + return ( <div className='modal fade' diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx index 3b10926f5..fc32d946b 100644 --- a/web/react/components/get_link_modal.jsx +++ b/web/react/components/get_link_modal.jsx @@ -2,69 +2,114 @@ // See License.txt for license information. var UserStore = require('../stores/user_store.jsx'); -var ZeroClipboardMixin = require('react-zeroclipboard-mixin'); -ZeroClipboardMixin.ZeroClipboard.config({ - swfPath: '../../static/flash/ZeroClipboard.swf' -}); +export default class GetLinkModal extends React.Component { + constructor(props) { + super(props); -module.exports = React.createClass({ - displayName: 'GetLinkModal', - zeroclipboardElementsSelector: '[data-copy-btn]', - mixins: [ZeroClipboardMixin], - componentDidMount: function() { - var self = this; + this.handleClick = this.handleClick.bind(this); + + this.state = {copiedLink: false}; + } + componentDidMount() { 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')}); - }); - $(this.refs.modal.getDOMNode()).on('hide.bs.modal', function() { - self.setState({copiedLink: false}); - }); + $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function show(e) { + var button = e.relatedTarget; + this.setState({title: $(button).attr('data-title'), value: $(button).attr('data-value')}); + }.bind(this)); + $(React.findDOMNode(this.refs.modal)).on('hide.bs.modal', function hide() { + this.setState({copiedLink: false}); + }.bind(this)); + } + } + handleClick() { + var copyTextarea = $(React.findDOMNode(this.refs.textarea)); + copyTextarea.select(); + + try { + var successful = document.execCommand('copy'); + if (successful) { + this.setState({copiedLink: true}); + } else { + this.setState({copiedLink: false}); + } + } catch (err) { + this.setState({copiedLink: false}); } - }, - getInitialState: function() { - return {copiedLink: false}; - }, - handleClick: function() { - this.setState({copiedLink: true}); - }, - render: function() { + } + render() { var currentUser = UserStore.getCurrentUser(); var copyLinkConfirm = null; if (this.state.copiedLink) { - copyLinkConfirm = <p className='alert alert-success copy-link-confirm'><i className="fa fa-check"></i> Link copied to clipboard.</p>; + copyLinkConfirm = <p className='alert alert-success copy-link-confirm'><i className='fa fa-check'></i> 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> - <div className='modal-body'> - <p> - Send {strings.Team + 'mates'} the link below for them to sign-up to this {strings.Team} site. - <br /><br /> - Be careful not to share this link publicly, since anyone with the link can join your {strings.Team}. - </p> - <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='true' type='button' className='btn btn-primary pull-left' onClick={this.handleClick} data-clipboard-text={this.state.value}>Copy Link</button> - {copyLinkConfirm} + <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> + Send {strings.Team + 'mates'} the link below for them to sign-up to this {strings.Team} site. + <br /><br /> + Be careful not to share this link publicly, since anyone with the link can join your {strings.Team}. + </p> + <textarea + className='form-control no-resize' + readOnly='true' + ref='textarea' + value={this.state.value} + /> + </div> + <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> + </div> </div> ); } return <div/>; } -}); +} diff --git a/web/react/components/loading_screen.jsx b/web/react/components/loading_screen.jsx index 5905e519b..b0f42ce86 100644 --- a/web/react/components/loading_screen.jsx +++ b/web/react/components/loading_screen.jsx @@ -1,24 +1,31 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -module.exports = React.createClass({ - displayName: "LoadingScreen", - propTypes: { - position: React.PropTypes.oneOf(['absolute', 'fixed', 'relative', 'static', 'inherit']) - }, - getDefaultProps: function() { - return { position: 'relative' }; - }, - render: function() { +export default class LoadingScreen extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + render() { return ( - <div className="loading-screen" style={{position: this.props.position}}> - <div className="loading__content"> + <div + className='loading-screen' + style={{position: this.props.position}} + > + <div className='loading__content'> <h3>Loading</h3> - <div className="round round-1"></div> - <div className="round round-2"></div> - <div className="round round-3"></div> + <div className='round round-1'></div> + <div className='round round-2'></div> + <div className='round round-3'></div> </div> </div> ); } -}); +} + +LoadingScreen.defaultProps = { + position: 'relative' +}; +LoadingScreen.propTypes = { + position: React.PropTypes.oneOf(['absolute', 'fixed', 'relative', 'static', 'inherit']) +}; diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx index 06c373e5d..d6cf4f9d6 100644 --- a/web/react/components/navbar.jsx +++ b/web/react/components/navbar.jsx @@ -1,7 +1,7 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var client = require('../utils/client.jsx'); +var Client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var UserStore = require('../stores/user_store.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); @@ -13,22 +13,27 @@ var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -function getStateFromStores() { - return { - channel: ChannelStore.getCurrent(), - member: ChannelStore.getCurrentMember(), - users: ChannelStore.getCurrentExtraInfo().members - }; -} +export default class Navbar extends React.Component { + constructor(props) { + super(props); + + this.onChange = this.onChange.bind(this); + this.handleLeave = this.handleLeave.bind(this); + this.createCollapseButtons = this.createCollapseButtons.bind(this); + this.createDropdown = this.createDropdown.bind(this); -module.exports = React.createClass({ - displayName: 'Navbar', - propTypes: { - teamDisplayName: React.PropTypes.string - }, - componentDidMount: function() { - ChannelStore.addChangeListener(this.onListenerChange); - ChannelStore.addExtraInfoChangeListener(this.onListenerChange); + this.state = this.getStateFromStores(); + } + getStateFromStores() { + return { + channel: ChannelStore.getCurrent(), + member: ChannelStore.getCurrentMember(), + users: ChannelStore.getCurrentExtraInfo().members + }; + } + componentDidMount() { + ChannelStore.addChangeListener(this.onChange); + ChannelStore.addExtraInfoChangeListener(this.onChange); $('.inner__wrap').click(this.hideSidebars); $('body').on('click.infopopover', function handlePopoverClick(e) { @@ -36,15 +41,15 @@ module.exports = React.createClass({ $('.info-popover').popover('hide'); } }); - }, - componentWillUnmount: function() { - ChannelStore.removeChangeListener(this.onListenerChange); - }, - handleSubmit: function(e) { + } + componentWillUnmount() { + ChannelStore.removeChangeListener(this.onChange); + } + handleSubmit(e) { e.preventDefault(); - }, - handleLeave: function() { - client.leaveChannel(this.state.channel.id, + } + handleLeave() { + Client.leaveChannel(this.state.channel.id, function success() { AsyncClient.getChannels(true); window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square'; @@ -53,8 +58,8 @@ module.exports = React.createClass({ AsyncClient.dispatchError(err, 'handleLeave'); } ); - }, - hideSidebars: function(e) { + } + hideSidebars(e) { var windowWidth = $(window).outerWidth(); if (windowWidth <= 768) { AppDispatcher.handleServerAction({ @@ -74,32 +79,259 @@ module.exports = React.createClass({ $('.sidebar--menu').removeClass('move--left'); } } - }, - toggleLeftSidebar: function() { + } + toggleLeftSidebar() { $('.inner__wrap').toggleClass('move--right'); $('.sidebar--left').toggleClass('move--right'); - }, - toggleRightSidebar: function() { + } + toggleRightSidebar() { $('.inner__wrap').toggleClass('move--left-small'); $('.sidebar--menu').toggleClass('move--left'); - }, - onListenerChange: function() { - this.setState(getStateFromStores()); + } + onChange() { + this.setState(this.getStateFromStores()); $('#navbar .navbar-brand .description').popover({placement: 'bottom', trigger: 'click', html: true}); - }, - getInitialState: function() { - return getStateFromStores(); - }, - render: function() { + } + createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent) { + if (channel) { + var viewInfoOption = ( + <li role='presentation'> + <a + role='menuitem' + data-toggle='modal' + data-target='#channel_info' + data-channelid={channel.id} + href='#' + > + View Info + </a> + </li> + ); + + var setChannelDescriptionOption = ( + <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> + ); + + var addMembersOption; + var leaveChannelOption; + if (!isDirect && !ChannelStore.isDefault(channel)) { + addMembersOption = ( + <li role='presentation'> + <a + role='menuitem' + data-toggle='modal' + data-target='#channel_invite' + href='#' + > + Add Members + </a> + </li> + ); + + leaveChannelOption = ( + <li role='presentation'> + <a + role='menuitem' + href='#' + onClick={this.handleLeave} + > + Leave Channel + </a> + </li> + ); + } + + var manageMembersOption; + var renameChannelOption; + var deleteChannelOption; + if (!isDirect && isAdmin && !ChannelStore.isDefault(channel)) { + manageMembersOption = ( + <li role='presentation'> + <a + role='menuitem' + data-toggle='modal' + data-target='#channel_members' + href='#' + > + Manage Members + </a> + </li> + ); + + renameChannelOption = ( + <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> + ); + + deleteChannelOption = ( + <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> + ); + } + + var notificationPreferenceOption; + if (!isDirect) { + notificationPreferenceOption = ( + <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> + ); + } + + return ( + <div className='navbar-brand'> + <div className='dropdown'> + <div + data-toggle='popover' + data-content={popoverContent} + className='description info-popover' + /> + <a + href='#' + className='dropdown-toggle theme' + type='button' + id='channel_header_dropdown' + data-toggle='dropdown' + aria-expanded='true' + > + <span className='heading'>{channelTitle} </span> + <span className='glyphicon glyphicon-chevron-down header-dropdown__icon'></span> + </a> + <ul + className='dropdown-menu' + role='menu' + aria-labelledby='channel_header_dropdown' + > + {viewInfoOption} + {addMembersOption} + {manageMembersOption} + {setChannelDescriptionOption} + {notificationPreferenceOption} + {renameChannelOption} + {deleteChannelOption} + {leaveChannelOption} + </ul> + </div> + </div> + ); + } + + return ( + <div className='navbar-brand'> + <a + href='/' + className='heading' + > + {channelTitle} + </a> + </div> + ); + } + createCollapseButtons(currentId) { + var buttons = []; + if (currentId == null) { + buttons.push( + <button + type='button' + className='navbar-toggle' + data-toggle='collapse' + data-target='#navbar-collapse-1' + > + <span className='sr-only'>Toggle sidebar</span> + <span className='icon-bar'></span> + <span className='icon-bar'></span> + <span className='icon-bar'></span> + </button> + ); + } else { + buttons.push( + <button + type='button' + className='navbar-toggle' + data-toggle='collapse' + data-target='#sidebar-nav' + onClick={this.toggleLeftSidebar} + > + <span className='sr-only'>Toggle sidebar</span> + <span className='icon-bar'></span> + <span className='icon-bar'></span> + <span className='icon-bar'></span> + <NotifyCounts /> + </button> + ); + + buttons.push( + <button + type='button' + className='navbar-toggle menu-toggle pull-right' + data-toggle='collapse' + data-target='#sidebar-nav' + onClick={this.toggleRightSidebar} + > + <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} /> + </button> + ); + } + + return buttons; + } + render() { var currentId = UserStore.getCurrentId(); - var popoverContent = ''; + var channel = this.state.channel; var channelTitle = this.props.teamDisplayName; + var popoverContent; var isAdmin = false; var isDirect = false; - var channel = this.state.channel; if (channel) { - popoverContent = React.renderToString(<MessageWrapper message={channel.description} options={{singleline: true, noMentionHighlight: true}}/>); + popoverContent = React.renderToString( + <MessageWrapper + message={channel.description} + options={{singleline: true, noMentionHighlight: true}} + /> + ); isAdmin = this.state.member.roles.indexOf('admin') > -1; if (channel.type === 'O') { @@ -118,110 +350,46 @@ module.exports = React.createClass({ } if (channel.description.length === 0) { - popoverContent = React.renderToString(<div>No channel description yet. <br /><a href='#' data-toggle='modal' data-desc={channel.description} data-title={channel.display_name} data-channelid={channel.id} data-target='#edit_channel'>Click here</a> to add one.</div>); + popoverContent = React.renderToString( + <div> + No channel description yet. <br/> + <a + href='#' + data-toggle='modal' + data-desc={channel.description} + data-title={channel.display_name} + data-channelid={channel.id} + data-target='#edit_channel' + > + Click here + </a> to add one.</div> + ); } } - var navbarCollapseButton = null; - if (currentId == null) { - navbarCollapseButton = (<button type='button' className='navbar-toggle' data-toggle='collapse' data-target='#navbar-collapse-1'> - <span className='sr-only'>Toggle sidebar</span> - <span className='icon-bar'></span> - <span className='icon-bar'></span> - <span className='icon-bar'></span> - </button>); - } - - var sidebarCollapseButton = null; - if (currentId != null) { - sidebarCollapseButton = (<button type='button' className='navbar-toggle' data-toggle='collapse' data-target='#sidebar-nav' onClick={this.toggleLeftSidebar}> - <span className='sr-only'>Toggle sidebar</span> - <span className='icon-bar'></span> - <span className='icon-bar'></span> - <span className='icon-bar'></span> - <NotifyCounts /> - </button>); - } - - var rightSidebarCollapseButton = null; - if (currentId != null) { - rightSidebarCollapseButton = (<button type='button' className='navbar-toggle menu-toggle pull-right' data-toggle='collapse' data-target='#sidebar-nav' onClick={this.toggleRightSidebar}> - <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} /> - </button>); - } - - var channelMenuDropdown = null; - if (channel) { - var viewInfoOption = <li role='presentation'><a role='menuitem' data-toggle='modal' data-target='#channel_info' data-channelid={channel.id} href='#'>View Info</a></li> - - var addMembersOption = null; - if (!isDirect && !ChannelStore.isDefault(channel)) { - addMembersOption = <li role='presentation'><a role='menuitem' data-toggle='modal' data-target='#channel_invite' href='#'>Add Members</a></li>; - } - - var manageMembersOption = null; - if (!isDirect && isAdmin && !ChannelStore.isDefault(channel)) { - manageMembersOption = <li role='presentation'><a role='menuitem' data-toggle='modal' data-target='#channel_members' href='#'>Manage Members</a></li>; - } - - var setChannelDescriptionOption = <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>; - - var notificationPreferenceOption = null; - if (!isDirect) { - notificationPreferenceOption = <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>; - } - - var renameChannelOption = null; - if (!isDirect && isAdmin && !ChannelStore.isDefault(channel)) { - renameChannelOption = <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>; - } - - var deleteChannelOption = null; - if (!isDirect && isAdmin && !ChannelStore.isDefault(channel)) { - deleteChannelOption = <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>; - } + var collapseButtons = this.createCollapseButtons(currentId); - var leaveChannelOption = null; - if (!isDirect && !ChannelStore.isDefault(channel)) { - leaveChannelOption = <li role='presentation'><a role='menuitem' href='#' onClick={this.handleLeave}>Leave Channel</a></li>; - } - - channelMenuDropdown = (<div className='navbar-brand'> - <div className='dropdown'> - <div data-toggle='popover' data-content={popoverContent} className='description info-popover'></div> - <a href='#' className='dropdown-toggle theme' type='button' id='channel_header_dropdown' data-toggle='dropdown' aria-expanded='true'> - <span className='heading'>{channelTitle} </span> - <span className='glyphicon glyphicon-chevron-down header-dropdown__icon'></span> - </a> - <ul className='dropdown-menu' role='menu' aria-labelledby='channel_header_dropdown'> - {viewInfoOption} - {addMembersOption} - {manageMembersOption} - {setChannelDescriptionOption} - {notificationPreferenceOption} - {renameChannelOption} - {deleteChannelOption} - {leaveChannelOption} - </ul> - </div> - </div>); - } else { - channelMenuDropdown = (<div className='navbar-brand'> - <a href='/' className='heading'>{channelTitle}</a> - </div>); - } + var channelMenuDropdown = this.createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent); return ( - <nav className='navbar navbar-default navbar-fixed-top' role='navigation'> + <nav + className='navbar navbar-default navbar-fixed-top' + role='navigation' + > <div className='container-fluid theme'> <div className='navbar-header'> - {navbarCollapseButton} - {sidebarCollapseButton} - {rightSidebarCollapseButton} + {collapseButtons} {channelMenuDropdown} </div> </div> </nav> ); } -}); +} + +Navbar.defaultProps = { + teamDisplayName: '' +}; +Navbar.propTypes = { + teamDisplayName: React.PropTypes.string +}; diff --git a/web/react/components/post_header.jsx b/web/react/components/post_header.jsx index 129db6d14..9dc525e03 100644 --- a/web/react/components/post_header.jsx +++ b/web/react/components/post_header.jsx @@ -1,23 +1,42 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var UserProfile = require( './user_profile.jsx' ); +var UserProfile = require('./user_profile.jsx'); var PostInfo = require('./post_info.jsx'); -module.exports = React.createClass({ - getInitialState: function() { - return { }; - }, - render: function() { +export default class PostHeader extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + render() { var post = this.props.post; return ( - <ul className="post-header post-header-post"> - <li className="post-header-col post-header__name"><strong><UserProfile userId={post.user_id} /></strong></li> - <li className="post-info--hidden"> - <PostInfo post={post} commentCount={this.props.commentCount} handleCommentClick={this.props.handleCommentClick} allowReply="true" isLastComment={this.props.isLastComment} /> + <ul className='post-header post-header-post'> + <li className='post-header-col post-header__name'><strong><UserProfile userId={post.user_id} /></strong></li> + <li className='post-info--hidden'> + <PostInfo + post={post} + commentCount={this.props.commentCount} + handleCommentClick={this.props.handleCommentClick} + allowReply='true' + isLastComment={this.props.isLastComment} + /> </li> </ul> ); } -}); +} + +PostHeader.defaultProps = { + post: null, + commentCount: 0, + isLastComment: false +}; +PostHeader.propTypes = { + post: React.PropTypes.object, + commentCount: React.PropTypes.number, + isLastComment: React.PropTypes.bool, + handleCommentClick: React.PropTypes.func +}; diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 5b74165f3..ef23f5bc2 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -8,127 +8,137 @@ 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 utils = require('../utils/utils.jsx'); +var Utils = require('../utils/utils.jsx'); var SidebarHeader = require('./sidebar_header.jsx'); var SearchBox = require('./search_bar.jsx'); var Constants = require('../utils/constants.jsx'); -function getStateFromStores() { - var members = ChannelStore.getAllMembers(); - var teamMemberMap = UserStore.getActiveOnlyProfiles(); - var currentId = ChannelStore.getCurrentId(); +export default class Sidebar extends React.Component { + constructor(props) { + super(props); - var teammates = []; - for (var id in teamMemberMap) { - if (id === UserStore.getCurrentId()) { - continue; - } - teammates.push(teamMemberMap[id]); - } + this.badgesActive = false; + this.firstUnreadChannel = null; + this.lastUnreadChannel = null; - // Create lists of all read and unread direct channels - var showDirectChannels = []; - var readDirectChannels = []; - for (var i = 0; i < teammates.length; i++) { - var teammate = teammates[i]; + this.onChange = this.onChange.bind(this); + this.onScroll = this.onScroll.bind(this); + this.onResize = this.onResize.bind(this); + this.updateUnreadIndicators = this.updateUnreadIndicators.bind(this); + this.createChannelElement = this.createChannelElement.bind(this); - if (teammate.id === UserStore.getCurrentId()) { - continue; + this.state = this.getStateFromStores(); + this.state.loadingDMChannel = -1; + } + getStateFromStores() { + var members = ChannelStore.getAllMembers(); + var teamMemberMap = UserStore.getActiveOnlyProfiles(); + var currentId = ChannelStore.getCurrentId(); + + var teammates = []; + for (var id in teamMemberMap) { + if (id === UserStore.getCurrentId()) { + continue; + } + teammates.push(teamMemberMap[id]); } - var channelName = ''; - if (teammate.id > UserStore.getCurrentId()) { - channelName = UserStore.getCurrentId() + '__' + teammate.id; - } else { - channelName = teammate.id + '__' + UserStore.getCurrentId(); - } + // Create lists of all read and unread direct channels + var showDirectChannels = []; + var readDirectChannels = []; + for (var i = 0; i < teammates.length; i++) { + var teammate = teammates[i]; - var channel = ChannelStore.getByName(channelName); + if (teammate.id === UserStore.getCurrentId()) { + continue; + } - if (channel != null) { - channel.display_name = teammate.username; - channel.teammate_username = teammate.username; + var channelName = ''; + if (teammate.id > UserStore.getCurrentId()) { + channelName = UserStore.getCurrentId() + '__' + teammate.id; + } else { + channelName = teammate.id + '__' + UserStore.getCurrentId(); + } - channel.status = UserStore.getStatus(teammate.id); + var channel = ChannelStore.getByName(channelName); - var channelMember = members[channel.id]; - var msgCount = channel.total_msg_count - channelMember.msg_count; - if (msgCount > 0) { - showDirectChannels.push(channel); - } else if (currentId === channel.id) { - showDirectChannels.push(channel); + if (channel != null) { + channel.display_name = teammate.username; + channel.teammate_username = teammate.username; + + channel.status = UserStore.getStatus(teammate.id); + + var channelMember = members[channel.id]; + var msgCount = channel.total_msg_count - channelMember.msg_count; + if (msgCount > 0) { + showDirectChannels.push(channel); + } else if (currentId === channel.id) { + showDirectChannels.push(channel); + } else { + readDirectChannels.push(channel); + } } else { - readDirectChannels.push(channel); + var tempChannel = {}; + tempChannel.fake = true; + tempChannel.name = channelName; + tempChannel.display_name = teammate.username; + tempChannel.teammate_username = teammate.username; + tempChannel.status = UserStore.getStatus(teammate.id); + tempChannel.last_post_at = 0; + tempChannel.total_msg_count = 0; + tempChannel.type = 'D'; + readDirectChannels.push(tempChannel); } - } else { - var tempChannel = {}; - tempChannel.fake = true; - tempChannel.name = channelName; - tempChannel.display_name = teammate.username; - tempChannel.teammate_username = teammate.username; - tempChannel.status = UserStore.getStatus(teammate.id); - tempChannel.last_post_at = 0; - tempChannel.total_msg_count = 0; - tempChannel.type = 'D'; - readDirectChannels.push(tempChannel); } - } - // 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 sortByLastPost(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 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 sortByLastPost(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 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 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; + } + return 0; + }); + + var index = 0; + while (showDirectChannels.length < Constants.MAX_DMS && index < readDirectChannels.length) { + showDirectChannels.push(readDirectChannels[index]); + index++; } - return 0; - }); + readDirectChannels = readDirectChannels.slice(index); - var index = 0; - while (showDirectChannels.length < Constants.MAX_DMS && index < readDirectChannels.length) { - showDirectChannels.push(readDirectChannels[index]); - index++; + showDirectChannels.sort(function directSort(a, b) { + if (a.display_name < b.display_name) { + return -1; + } + if (a.display_name > b.display_name) { + return 1; + } + return 0; + }); } - readDirectChannels = readDirectChannels.slice(index); - showDirectChannels.sort(function directSort(a, b) { - if (a.display_name < b.display_name) { - return -1; - } - if (a.display_name > b.display_name) { - return 1; - } - return 0; - }); + return { + activeId: currentId, + channels: ChannelStore.getAll(), + members: members, + showDirectChannels: showDirectChannels, + hideDirectChannels: readDirectChannels + }; } - - return { - activeId: currentId, - channels: ChannelStore.getAll(), - members: members, - showDirectChannels: showDirectChannels, - hideDirectChannels: readDirectChannels - }; -} - -module.exports = React.createClass({ - displayName: 'Sidebar', - propTypes: { - teamType: React.PropTypes.string, - teamDisplayName: React.PropTypes.string - }, - componentDidMount: function() { + componentDidMount() { ChannelStore.addChangeListener(this.onChange); UserStore.addChangeListener(this.onChange); UserStore.addStatusesChangeListener(this.onChange); @@ -140,12 +150,12 @@ module.exports = React.createClass({ this.updateUnreadIndicators(); $(window).on('resize', this.onResize); - }, - componentDidUpdate: function() { + } + componentDidUpdate() { this.updateTitle(); this.updateUnreadIndicators(); - }, - componentWillUnmount: function() { + } + componentWillUnmount() { $(window).off('resize', this.onResize); ChannelStore.removeChangeListener(this.onChange); @@ -153,14 +163,14 @@ module.exports = React.createClass({ UserStore.removeStatusesChangeListener(this.onChange); TeamStore.removeChangeListener(this.onChange); SocketStore.removeChangeListener(this.onSocketChange); - }, - onChange: function() { - var newState = getStateFromStores(); - if (!utils.areStatesEqual(newState, this.state)) { + } + onChange() { + var newState = this.getStateFromStores(); + if (!Utils.areStatesEqual(newState, this.state)) { this.setState(newState); } - }, - onSocketChange: function(msg) { + } + onSocketChange(msg) { if (msg.action === 'posted') { if (ChannelStore.getCurrentId() === msg.channel_id) { if (window.isActive) { @@ -208,17 +218,17 @@ module.exports = React.createClass({ if (notifyText.length === 0) { if (msgProps.image) { - utils.notifyMe(title, username + ' uploaded an image', channel); + 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 a file', channel); } else { - utils.notifyMe(title, username + ' did something new', channel); + Utils.notifyMe(title, username + ' did something new', channel); } } else { - utils.notifyMe(title, username + ' wrote: ' + notifyText, channel); + Utils.notifyMe(title, username + ' wrote: ' + notifyText, channel); } if (!user.notify_props || user.notify_props.desktop_sound === 'true') { - utils.ding(); + Utils.ding(); } } } else if (msg.action === 'viewed') { @@ -243,186 +253,196 @@ module.exports = React.createClass({ } } } - }, - updateTitle: function() { + } + updateTitle() { var channel = ChannelStore.getCurrent(); if (channel) { if (channel.type === 'D') { - var teammateUsername = utils.getDirectTeammate(channel.id).username; + var teammateUsername = Utils.getDirectTeammate(channel.id).username; document.title = teammateUsername + ' ' + document.title.substring(document.title.lastIndexOf('-')); } else { document.title = channel.display_name + ' ' + document.title.substring(document.title.lastIndexOf('-')); } } - }, - onScroll: function() { + } + onScroll() { this.updateUnreadIndicators(); - }, - onResize: function() { + } + onResize() { this.updateUnreadIndicators(); - }, - updateUnreadIndicators: function() { - var container = $(this.refs.container.getDOMNode()); + } + updateUnreadIndicators() { + var container = $(React.findDOMNode(this.refs.container)); if (this.firstUnreadChannel) { - var firstUnreadElement = $(this.refs[this.firstUnreadChannel].getDOMNode()); + var firstUnreadElement = $(React.findDOMNode(this.refs[this.firstUnreadChannel])); if (firstUnreadElement.position().top + firstUnreadElement.height() < 0) { - $(this.refs.topUnreadIndicator.getDOMNode()).css('display', 'initial'); + $(React.findDOMNode(this.refs.topUnreadIndicator)).css('display', 'initial'); } else { - $(this.refs.topUnreadIndicator.getDOMNode()).css('display', 'none'); + $(React.findDOMNode(this.refs.topUnreadIndicator)).css('display', 'none'); } } if (this.lastUnreadChannel) { - var lastUnreadElement = $(this.refs[this.lastUnreadChannel].getDOMNode()); + var lastUnreadElement = $(React.findDOMNode(this.refs[this.lastUnreadChannel])); if (lastUnreadElement.position().top > container.height()) { - $(this.refs.bottomUnreadIndicator.getDOMNode()).css('display', 'initial'); + $(React.findDOMNode(this.refs.bottomUnreadIndicator)).css('display', 'initial'); } else { - $(this.refs.bottomUnreadIndicator.getDOMNode()).css('display', 'none'); + $(React.findDOMNode(this.refs.bottomUnreadIndicator)).css('display', 'none'); } } - }, - getInitialState: function() { - var newState = getStateFromStores(); - newState.loadingDMChannel = -1; - - return newState; - }, - render: function() { + } + createChannelElement(channel, index) { var members = this.state.members; var activeId = this.state.activeId; - var badgesActive = false; + var channelMember = members[channel.id]; + var msgCount; - // keep track of the first and last unread channels so we can use them to set the unread indicators - var self = this; - this.firstUnreadChannel = null; - this.lastUnreadChannel = null; - - function createChannelElement(channel, index) { - var channelMember = members[channel.id]; - var msgCount; - - var linkClass = ''; - if (channel.id === activeId) { - linkClass = 'active'; - } + var linkClass = ''; + if (channel.id === activeId) { + linkClass = 'active'; + } - var unread = false; - if (channelMember) { - msgCount = channel.total_msg_count - channelMember.msg_count; - unread = (msgCount > 0 && channelMember.notify_level !== 'quiet') || channelMember.mention_count > 0; - } + var unread = false; + if (channelMember) { + msgCount = channel.total_msg_count - channelMember.msg_count; + unread = (msgCount > 0 && channelMember.notify_level !== 'quiet') || channelMember.mention_count > 0; + } - var titleClass = ''; - if (unread) { - titleClass = 'unread-title'; + var titleClass = ''; + if (unread) { + titleClass = 'unread-title'; - if (!self.firstUnreadChannel) { - self.firstUnreadChannel = channel.name; - } - self.lastUnreadChannel = channel.name; - } - - var badge = null; - if (channelMember) { - if (channel.type === 'D') { - // direct message channels show badges for any number of unread posts - msgCount = channel.total_msg_count - channelMember.msg_count; - if (msgCount > 0) { - badge = <span className='badge pull-right small'>{msgCount}</span>; - badgesActive = true; - } - } else if (channelMember.mention_count > 0) { - // public and private channels only show badges for mentions - badge = <span className='badge pull-right small'>{channelMember.mention_count}</span>; - badgesActive = true; - } - } else if (self.state.loadingDMChannel === index && channel.type === 'D') { - badge = <img className='channel-loading-gif pull-right' src='/static/images/load.gif'/>; + if (!this.firstUnreadChannel) { + this.firstUnreadChannel = channel.name; } + this.lastUnreadChannel = channel.name; + } - // set up status icon for direct message channels - var status = null; + var badge = null; + if (channelMember) { if (channel.type === 'D') { - var statusIcon = ''; - if (channel.status === 'online') { - statusIcon = Constants.ONLINE_ICON_SVG; - } else if (channel.status === 'away') { - statusIcon = Constants.ONLINE_ICON_SVG; - } else { - statusIcon = Constants.OFFLINE_ICON_SVG; + // direct message channels show badges for any number of unread posts + msgCount = channel.total_msg_count - channelMember.msg_count; + if (msgCount > 0) { + badge = <span className='badge pull-right small'>{msgCount}</span>; + this.badgesActive = true; } - status = <span className='status' dangerouslySetInnerHTML={{__html: statusIcon}} />; + } else if (channelMember.mention_count > 0) { + // public and private channels only show badges for mentions + badge = <span className='badge pull-right small'>{channelMember.mention_count}</span>; + this.badgesActive = true; } + } else if (this.state.loadingDMChannel === index && channel.type === 'D') { + badge = ( + <img + className='channel-loading-gif pull-right' + src='/static/images/load.gif' + /> + ); + } - // set up click handler to switch channels (or create a new channel for non-existant ones) - var handleClick = null; - var href = '#'; - var teamURL = TeamStore.getCurrentTeamUrl(); + // set up status icon for direct message channels + var status = null; + if (channel.type === 'D') { + var statusIcon = ''; + if (channel.status === 'online') { + statusIcon = Constants.ONLINE_ICON_SVG; + } else if (channel.status === 'away') { + statusIcon = Constants.ONLINE_ICON_SVG; + } else { + statusIcon = Constants.OFFLINE_ICON_SVG; + } + status = ( + <span + className='status' + dangerouslySetInnerHTML={{__html: statusIcon}} + /> + ); + } - if (!channel.fake) { + // set up click handler to switch channels (or create a new channel for non-existant ones) + var handleClick = null; + var href = '#'; + var teamURL = TeamStore.getCurrentTeamUrl(); + + if (!channel.fake) { + handleClick = function clickHandler(e) { + e.preventDefault(); + Utils.switchChannel(channel); + }; + } else if (channel.fake && teamURL) { + // It's a direct message channel that doesn't exist yet so let's create it now + var otherUserId = Utils.getUserIdFromChannelName(channel); + + if (this.state.loadingDMChannel === -1) { handleClick = function clickHandler(e) { e.preventDefault(); - utils.switchChannel(channel); + this.setState({loadingDMChannel: index}); + + Client.createDirectChannel(channel, otherUserId, + function success(data) { + this.setState({loadingDMChannel: -1}); + AsyncClient.getChannel(data.id); + Utils.switchChannel(data); + }, + function error() { + this.setState({loadingDMChannel: -1}); + window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name; + } + ); }; - } else if (channel.fake && teamURL) { - // It's a direct message channel that doesn't exist yet so let's create it now - var otherUserId = utils.getUserIdFromChannelName(channel); - - if (self.state.loadingDMChannel === -1) { - handleClick = function clickHandler(e) { - e.preventDefault(); - self.setState({loadingDMChannel: index}); - - Client.createDirectChannel(channel, otherUserId, - function success(data) { - self.setState({loadingDMChannel: -1}); - AsyncClient.getChannel(data.id); - utils.switchChannel(data); - }, - function error() { - self.setState({loadingDMChannel: -1}); - window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name; - } - ); - }; - } } - - return ( - <li key={channel.name} ref={channel.name} className={linkClass}> - <a className={'sidebar-channel ' + titleClass} href={href} onClick={handleClick}> - {status} - {channel.display_name} - {badge} - </a> - </li> - ); } + return ( + <li + key={channel.name} + ref={channel.name} + className={linkClass} + > + <a + className={'sidebar-channel ' + titleClass} + href={href} + onClick={handleClick} + > + {status} + {channel.display_name} + {badge} + </a> + </li> + ); + } + render() { + this.badgesActive = false; + + // keep track of the first and last unread channels so we can use them to set the unread indicators + this.firstUnreadChannel = null; + this.lastUnreadChannel = null; + // create elements for all 3 types of channels var channelItems = this.state.channels.filter( function filterPublicChannels(channel) { return channel.type === 'O'; } - ).map(createChannelElement); + ).map(this.createChannelElement); var privateChannelItems = this.state.channels.filter( function filterPrivateChannels(channel) { return channel.type === 'P'; } - ).map(createChannelElement); + ).map(this.createChannelElement); - var directMessageItems = this.state.showDirectChannels.map(createChannelElement); + var directMessageItems = this.state.showDirectChannels.map(this.createChannelElement); // update the favicon to show if there are any notifications var link = document.createElement('link'); link.type = 'image/x-icon'; link.rel = 'shortcut icon'; link.id = 'favicon'; - if (badgesActive) { + if (this.badgesActive) { link.href = '/static/images/redfavicon.ico'; } else { link.href = '/static/images/favicon.ico'; @@ -438,7 +458,13 @@ module.exports = React.createClass({ if (this.state.hideDirectChannels.length > 0) { directMessageMore = ( <li> - <a href='#' data-toggle='modal' className='nav-more' data-target='#more_direct_channels' data-channels={JSON.stringify(this.state.hideDirectChannels)}> + <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> @@ -447,21 +473,76 @@ module.exports = React.createClass({ return ( <div> - <SidebarHeader teamDisplayName={this.props.teamDisplayName} teamType={this.props.teamType} /> + <SidebarHeader + teamDisplayName={this.props.teamDisplayName} + teamType={this.props.teamType} + /> <SearchBox /> - <div ref='topUnreadIndicator' className='nav-pills__unread-indicator nav-pills__unread-indicator-top' style={{display: 'none'}}>Unread post(s) above</div> - <div ref='bottomUnreadIndicator' className='nav-pills__unread-indicator nav-pills__unread-indicator-bottom' style={{display: 'none'}}>Unread post(s) below</div> + <div + ref='topUnreadIndicator' + className='nav-pills__unread-indicator nav-pills__unread-indicator-top' + style={{display: 'none'}} + > + Unread post(s) above + </div> + <div + ref='bottomUnreadIndicator' + className='nav-pills__unread-indicator nav-pills__unread-indicator-bottom' + style={{display: 'none'}} + > + Unread post(s) below + </div> - <div ref='container' className='nav-pills__container' onScroll={this.onScroll}> + <div + ref='container' + className='nav-pills__container' + onScroll={this.onScroll} + > <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> + <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> + <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'> @@ -473,4 +554,13 @@ module.exports = React.createClass({ </div> ); } -}); +} + +Sidebar.defaultProps = { + teamType: '', + teamDisplayName: '' +}; +Sidebar.propTypes = { + teamType: React.PropTypes.string, + teamDisplayName: React.PropTypes.string +}; diff --git a/web/react/components/team_feature_tab.jsx b/web/react/components/team_feature_tab.jsx index 4f8f0b2cf..e5398332e 100644 --- a/web/react/components/team_feature_tab.jsx +++ b/web/react/components/team_feature_tab.jsx @@ -4,65 +4,65 @@ var SettingItemMin = require('./setting_item_min.jsx'); var SettingItemMax = require('./setting_item_max.jsx'); -var client = require('../utils/client.jsx'); +var Client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); -module.exports = React.createClass({ - displayName: 'Feature Tab', - propTypes: { - updateSection: React.PropTypes.func.isRequired, - team: React.PropTypes.object.isRequired, - activeSection: React.PropTypes.string.isRequired - }, - submitValetFeature: function() { +export default class FeatureTab extends React.Component { + constructor(props) { + super(props); + + this.submitValetFeature = this.submitValetFeature.bind(this); + this.handleValetRadio = this.handleValetRadio.bind(this); + this.onUpdateSection = this.onUpdateSection.bind(this); + + this.state = {}; + var team = this.props.team; + + if (team && team.allow_valet) { + this.state.allowValet = 'true'; + } else { + this.state.allowValet = 'false'; + } + } + componentWillReceiveProps(newProps) { + var team = newProps.team; + + var allowValet = 'false'; + if (team && team.allow_valet) { + allowValet = 'true'; + } + + this.setState({allowValet: allowValet}); + } + submitValetFeature() { var data = {}; data.allow_valet = this.state.allowValet; - client.updateValetFeature(data, - function() { + Client.updateValetFeature(data, + function success() { this.props.updateSection(''); AsyncClient.getMyTeam(); }.bind(this), - function(err) { + function fail(err) { var state = this.getInitialState(); state.serverError = err; this.setState(state); }.bind(this) ); - }, - handleValetRadio: function(val) { + } + handleValetRadio(val) { this.setState({allowValet: val}); - this.refs.wrapper.getDOMNode().focus(); - }, - componentWillReceiveProps: function(newProps) { - var team = newProps.team; - - var allowValet = 'false'; - if (team && team.allow_valet) { - allowValet = 'true'; - } - - this.setState({allowValet: allowValet}); - }, - getInitialState: function() { - var team = this.props.team; - - var allowValet = 'false'; - if (team && team.allow_valet) { - allowValet = 'true'; - } - - return {allowValet: allowValet}; - }, - onUpdateSection: function(e) { + React.findDOMNode(this.refs.wrapper).focus(); + } + onUpdateSection(e) { e.preventDefault(); if (this.props.activeSection === 'valet') { this.props.updateSection(''); } else { this.props.updateSection('valet'); } - }, - render: function() { + } + render() { var clientError = null; var serverError = null; if (this.state.clientError) { @@ -73,7 +73,6 @@ module.exports = React.createClass({ } var valetSection; - var self = this; if (this.props.activeSection === 'valet') { var valetActive = [false, false]; @@ -92,7 +91,7 @@ module.exports = React.createClass({ <input type='radio' checked={valetActive[0]} - onChange={self.handleValetRadio.bind(this, 'true')} + onChange={this.handleValetRadio.bind(this, 'true')} > On </input> @@ -104,7 +103,7 @@ module.exports = React.createClass({ <input type='radio' checked={valetActive[1]} - onChange={self.handleValetRadio.bind(this, 'false')} + onChange={this.handleValetRadio.bind(this, 'false')} > Off </input> @@ -145,10 +144,25 @@ module.exports = React.createClass({ return ( <div> <div className='modal-header'> - <button type='button' className='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>×</span></button> - <h4 className='modal-title' ref='title'><i className='modal-back'></i>Advanced Features</h4> + <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>Advanced Features + </h4> </div> - <div ref='wrapper' className='user-settings'> + <div + ref='wrapper' + className='user-settings' + > <h3 className='tab-header'>Advanced Features</h3> <div className='divider-dark first'/> {valetSection} @@ -157,4 +171,14 @@ module.exports = React.createClass({ </div> ); } -}); +} + +FeatureTab.defaultProps = { + team: {}, + activeSection: '' +}; +FeatureTab.propTypes = { + updateSection: React.PropTypes.func.isRequired, + team: React.PropTypes.object.isRequired, + activeSection: React.PropTypes.string.isRequired +}; diff --git a/web/react/components/team_settings.jsx b/web/react/components/team_settings.jsx index 1a79eef1d..53855fe1c 100644 --- a/web/react/components/team_settings.jsx +++ b/web/react/components/team_settings.jsx @@ -5,66 +5,83 @@ var TeamStore = require('../stores/team_store.jsx'); var ImportTab = require('./team_import_tab.jsx'); var FeatureTab = require('./team_feature_tab.jsx'); var GeneralTab = require('./team_general_tab.jsx'); -var utils = require('../utils/utils.jsx'); +var Utils = require('../utils/utils.jsx'); -module.exports = React.createClass({ - displayName: 'Team Settings', - propTypes: { - activeTab: React.PropTypes.string.isRequired, - activeSection: React.PropTypes.string.isRequired, - updateSection: React.PropTypes.func.isRequired, - teamDisplayName: React.PropTypes.string.isRequired - }, - componentDidMount: function() { +export default class TeamSettings extends React.Component { + constructor(props) { + super(props); + + this.onChange = this.onChange.bind(this); + + this.state = {team: TeamStore.getCurrent()}; + } + componentDidMount() { TeamStore.addChangeListener(this.onChange); - }, - componentWillUnmount: function() { + } + componentWillUnmount() { TeamStore.removeChangeListener(this.onChange); - }, - onChange: function() { + } + onChange() { var team = TeamStore.getCurrent(); - if (!utils.areStatesEqual(this.state.team, team)) { + if (!Utils.areStatesEqual(this.state.team, team)) { this.setState({team: team}); } - }, - getInitialState: function() { - return {team: TeamStore.getCurrent()}; - }, - render: function() { + } + render() { var result; switch (this.props.activeTab) { - case 'general': - result = ( - <div> - <GeneralTab - team={this.state.team} - activeSection={this.props.activeSection} - updateSection={this.props.updateSection} - teamDisplayName={this.props.teamDisplayName} - /> - </div> - ); - break; - case 'feature': - result = ( - <div> - <FeatureTab team={this.state.team} activeSection={this.props.activeSection} updateSection={this.props.updateSection} /> - </div> - ); - break; - case 'import': - result = ( - <div> - <ImportTab team={this.state.team} activeSection={this.props.activeSection} updateSection={this.props.updateSection} /> - </div> - ); - break; - default: - result = ( - <div/> - ); - break; + case 'general': + result = ( + <div> + <GeneralTab + team={this.state.team} + activeSection={this.props.activeSection} + updateSection={this.props.updateSection} + teamDisplayName={this.props.teamDisplayName} + /> + </div> + ); + break; + case 'feature': + result = ( + <div> + <FeatureTab + team={this.state.team} + activeSection={this.props.activeSection} + updateSection={this.props.updateSection} + /> + </div> + ); + break; + case 'import': + result = ( + <div> + <ImportTab + team={this.state.team} + activeSection={this.props.activeSection} + updateSection={this.props.updateSection} + /> + </div> + ); + break; + default: + result = ( + <div/> + ); + break; } return result; } -}); +} + +TeamSettings.defaultProps = { + activeTab: '', + activeSection: '', + teamDisplayName: '' +}; +TeamSettings.propTypes = { + activeTab: React.PropTypes.string.isRequired, + activeSection: React.PropTypes.string.isRequired, + updateSection: React.PropTypes.func.isRequired, + teamDisplayName: React.PropTypes.string.isRequired +}; diff --git a/web/react/components/team_signup_allowed_domains_page.jsx b/web/react/components/team_signup_allowed_domains_page.jsx index 90c7ff668..aee5afd23 100644 --- a/web/react/components/team_signup_allowed_domains_page.jsx +++ b/web/react/components/team_signup_allowed_domains_page.jsx @@ -1,31 +1,34 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var client = require('../utils/client.jsx'); +var Client = require('../utils/client.jsx'); -module.exports = React.createClass({ - displayName: 'TeamSignupAllowedDomainsPage', - propTypes: { - state: React.PropTypes.object, - updateParent: React.PropTypes.func - }, - submitBack: function(e) { +export default class TeamSignupAllowedDomainsPage extends React.Component { + constructor(props) { + super(props); + + this.submitBack = this.submitBack.bind(this); + this.submitNext = this.submitNext.bind(this); + + this.state = {}; + } + submitBack(e) { e.preventDefault(); this.props.state.wizard = 'team_url'; this.props.updateParent(this.props.state); - }, - submitNext: function(e) { + } + submitNext(e) { e.preventDefault(); - if (this.refs.open_network.getDOMNode().checked) { + if (React.findDOMNode(this.refs.open_network).checked) { this.props.state.wizard = 'send_invites'; this.props.state.team.type = 'O'; this.props.updateParent(this.props.state); return; } - if (this.refs.allow.getDOMNode().checked) { - var name = this.refs.name.getDOMNode().value.trim(); + if (React.findDOMNode(this.refs.allow).checked) { + var name = React.findDOMNode(this.refs.name).value.trim(); var domainRegex = /^\w+\.\w+$/; if (!name) { this.setState({nameError: 'This field is required'}); @@ -46,12 +49,9 @@ module.exports = React.createClass({ this.props.state.team.type = 'I'; this.props.updateParent(this.props.state); } - }, - getInitialState: function() { - return {}; - }, - render: function() { - client.track('signup', 'signup_team_04_allow_domains'); + } + render() { + Client.track('signup', 'signup_team_04_allow_domains'); var nameError = null; var nameDivClass = 'form-group'; @@ -63,11 +63,21 @@ module.exports = React.createClass({ return ( <div> <form> - <img className='signup-team-logo' src='/static/images/logo.png' /> + <img + className='signup-team-logo' + src='/static/images/logo.png' + /> <h2>Email Domain</h2> <p> <div className='checkbox'> - <label><input type='checkbox' ref='allow' defaultChecked={true} />{' Allow sign up and ' + strings.Team + ' discovery with a ' + strings.Company + ' email address.'}</label> + <label> + <input + type='checkbox' + ref='allow' + defaultChecked={true} + /> + {' Allow sign up and ' + strings.Team + ' discovery with a ' + strings.Company + ' email address.'} + </label> </div> </p> <p>{'Check this box to allow your ' + strings.Team + ' members to sign up using their ' + strings.Company + ' email addresses if you share the same domain--otherwise, you need to invite everyone yourself.'}</p> @@ -77,7 +87,16 @@ module.exports = React.createClass({ <div className='col-sm-9'> <div className='input-group'> <span className='input-group-addon'>@</span> - <input type='text' ref='name' className='form-control' placeholder='' maxLength='128' defaultValue={this.props.state.team.allowed_domains} autoFocus={true} onFocus={this.handleFocus}/> + <input + type='text' + ref='name' + className='form-control' + placeholder='' + maxLength='128' + defaultValue={this.props.state.team.allowed_domains} + autoFocus={true} + onFocus={this.handleFocus} + /> </div> </div> </div> @@ -86,13 +105,38 @@ module.exports = React.createClass({ <p>To allow signups from multiple domains, separate each with a comma.</p> <p> <div className='checkbox'> - <label><input type='checkbox' ref='open_network' defaultChecked={this.props.state.team.type === 'O'} /> Allow anyone to signup to this domain without an invitation.</label> + <label> + <input + type='checkbox' + ref='open_network' + defaultChecked={this.props.state.team.type === 'O'} + /> Allow anyone to signup to this domain without an invitation.</label> </div> </p> - <button type='button' className='btn btn-default' onClick={this.submitBack}><i className='glyphicon glyphicon-chevron-left'></i> Back</button> - <button type='submit' className='btn-primary btn' onClick={this.submitNext}>Next<i className='glyphicon glyphicon-chevron-right'></i></button> + <button + type='button' + className='btn btn-default' + onClick={this.submitBack} + > + <i className='glyphicon glyphicon-chevron-left'></i> Back + </button> + <button + type='submit' + className='btn-primary btn' + onClick={this.submitNext} + > + Next<i className='glyphicon glyphicon-chevron-right'></i> + </button> </form> </div> ); } -}); +} + +TeamSignupAllowedDomainsPage.defaultProps = { + state: {} +}; +TeamSignupAllowedDomainsPage.propTypes = { + state: React.PropTypes.object, + updateParent: React.PropTypes.func +}; diff --git a/web/react/components/team_signup_password_page.jsx b/web/react/components/team_signup_password_page.jsx index 18cf05dad..f36ec1119 100644 --- a/web/react/components/team_signup_password_page.jsx +++ b/web/react/components/team_signup_password_page.jsx @@ -1,24 +1,28 @@ // 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 Client = require('../utils/client.jsx'); +var BrowserStore = require('../stores/browser_store.jsx'); +var UserStore = require('../stores/user_store.jsx'); -module.exports = React.createClass({ - displayName: 'TeamSignupPasswordPage', - propTypes: { - state: React.PropTypes.object, - updateParent: React.PropTypes.func - }, - submitBack: function(e) { +export default class TeamSignupPasswordPage extends React.Component { + constructor(props) { + super(props); + + this.submitBack = this.submitBack.bind(this); + this.submitNext = this.submitNext.bind(this); + + this.state = {}; + } + submitBack(e) { e.preventDefault(); this.props.state.wizard = 'username'; this.props.updateParent(this.props.state); - }, - submitNext: function(e) { + } + submitNext(e) { e.preventDefault(); - var password = this.refs.password.getDOMNode().value.trim(); + var password = React.findDOMNode(this.refs.password).value.trim(); if (!password || password.length < 5) { this.setState({passwordError: 'Please enter at least 5 characters'}); return; @@ -31,15 +35,14 @@ module.exports = React.createClass({ teamSignup.user.allow_marketing = true; delete teamSignup.wizard; - client.createTeamFromSignup(teamSignup, + Client.createTeamFromSignup(teamSignup, function success() { - client.track('signup', 'signup_team_08_complete'); + Client.track('signup', 'signup_team_08_complete'); var props = this.props; - - client.loginByEmail(teamSignup.team.name, teamSignup.team.email, teamSignup.user.password, - function(data) { + Client.loginByEmail(teamSignup.team.name, teamSignup.team.email, teamSignup.user.password, + function loginSuccess() { UserStore.setLastEmail(teamSignup.team.email); UserStore.setCurrentUser(data); if (this.props.hash > 0) { @@ -52,7 +55,7 @@ module.exports = React.createClass({ window.location.href = '/'; }.bind(this), - function(err) { + function loginFail(err) { if (err.message === 'Login failed because email address has not been verified') { window.location.href = '/verify_email?email=' + encodeURIComponent(teamSignup.team.email) + '&teamname=' + encodeURIComponent(teamSignup.team.name); } else { @@ -67,12 +70,9 @@ module.exports = React.createClass({ $('#finish-button').button('reset'); }.bind(this) ); - }, - getInitialState: function() { - return {}; - }, - render: function() { - client.track('signup', 'signup_team_07_password'); + } + render() { + Client.track('signup', 'signup_team_07_password'); var passwordError = null; var passwordDivStyle = 'form-group'; @@ -89,7 +89,10 @@ module.exports = React.createClass({ return ( <div> <form> - <img className='signup-team-logo' src='/static/images/logo.png' /> + <img + className='signup-team-logo' + src='/static/images/logo.png' + /> <h2 className='margin--less'>Your password</h2> <h5 className='color--light'>Select a password that you'll use to login with your email address:</h5> <div className='inner__content margin--extra'> @@ -99,7 +102,14 @@ module.exports = React.createClass({ <div className='row'> <div className='col-sm-11'> <h5><strong>Choose your password</strong></h5> - <input autoFocus={true} type='password' ref='password' className='form-control' placeholder='' maxLength='128' /> + <input + autoFocus={true} + type='password' + ref='password' + className='form-control' + placeholder='' + maxLength='128' + /> <div className='color--light form__hint'>Passwords must contain 5 to 50 characters. Your password will be strongest if it contains a mix of symbols, numbers, and upper and lowercase characters.</div> </div> </div> @@ -108,14 +118,37 @@ module.exports = React.createClass({ </div> </div> <div className='form-group'> - <button type='submit' className='btn btn-primary margin--extra' id='finish-button' data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Creating ' + strings.Team + '...'} onClick={this.submitNext}>Finish</button> + <button + type='submit' + className='btn btn-primary margin--extra' + id='finish-button' + data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Creating ' + strings.Team + '...'} + onClick={this.submitNext} + > + Finish + </button> </div> <p>By proceeding to create your account and use {config.SiteName}, you agree to our <a href={config.TermsLink}>Terms of Service</a> and <a href={config.PrivacyLink}>Privacy Policy</a>. If you do not agree, you cannot use {config.SiteName}.</p> <div className='margin--extra'> - <a href='#' onClick={this.submitBack}>Back to previous step</a> + <a + href='#' + onClick={this.submitBack} + > + Back to previous step + </a> </div> </form> </div> ); } -}); +} + +TeamSignupPasswordPage.defaultProps = { + state: {}, + hash: '' +}; +TeamSignupPasswordPage.propTypes = { + state: React.PropTypes.object, + hash: React.PropTypes.string, + updateParent: React.PropTypes.func +}; diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx index 5c4d26a23..0d10166f3 100644 --- a/web/react/components/user_profile.jsx +++ b/web/react/components/user_profile.jsx @@ -1,20 +1,9 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. - -var utils = require('../utils/utils.jsx'); +var Utils = require('../utils/utils.jsx'); var UserStore = require('../stores/user_store.jsx'); -function getStateFromStores(userId) { - var profile = UserStore.getProfile(userId); - - if (profile == null) { - return { profile: { id: "0", username: "..."} }; - } else { - return { profile: profile }; - } -} - var id = 0; function nextId() { @@ -22,49 +11,76 @@ function nextId() { return id; } +export default class UserProfile extends React.Component { + constructor(props) { + super(props); + + this.uniqueId = nextId(); + + this.state = this.getStateFromStores(this.props.userId); + } + getStateFromStores(userId) { + var profile = UserStore.getProfile(userId); + + if (profile == null) { + return {profile: {id: '0', username: '...'}}; + } -module.exports = React.createClass({ - uniqueId: null, - 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); - }, - _onChange: function(id) { - if (id == this.props.userId) { - var newState = getStateFromStores(this.props.userId); - if (!utils.areStatesEqual(newState, this.state)) { + return {profile: profile}; + } + componentDidMount() { + 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() { + UserStore.removeChangeListener(this.onChange); + } + onChange(userId) { + if (userId === this.props.userId) { + var newState = this.getStateFromStores(this.props.userId); + if (!Utils.areStatesEqual(newState, this.state)) { this.setState(newState); } } - }, - componentWillReceiveProps: function(nextProps) { - if (this.props.userId != nextProps.userId) { - this.setState(getStateFromStores(nextProps.userId)); + } + componentWillReceiveProps(nextProps) { + if (this.props.userId !== nextProps.userId) { + this.setState(this.getStateFromStores(nextProps.userId)); + } + } + render() { + var name = this.state.profile.username; + if (this.props.overwriteName) { + name = this.props.overwriteName; } - }, - getInitialState: function() { - this.uniqueId = nextId(); - return getStateFromStores(this.props.userId); - }, - render: function() { - var name = this.props.overwriteName ? this.props.overwriteName : this.state.profile.username; - - var data_content = "<img class='user-popover__image' src='/api/v1/users/" + this.state.profile.id + "/image?time=" + this.state.profile.update_at + "' height='128' width='128' />"; + var dataContent = '<img class="user-popover__image" src="/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at + '" height="128" width="128" />'; if (!config.ShowEmail) { - data_content += "<div class='text-nowrap'>Email not shared</div>"; + dataContent += '<div class="text-nowrap">Email not shared</div>'; } else { - 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>"; + dataContent += '<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 ( - <div className="user-popover" id={"profile_" + this.uniqueId} data-toggle="popover" data-content={data_content} data-original-title={this.state.profile.username} > - { name } + <div + className='user-popover' + id={'profile_' + this.uniqueId} + data-toggle='popover' + data-content={dataContent} + data-original-title={this.state.profile.username} + > + {name} </div> ); } -}); +} + +UserProfile.defaultProps = { + userId: '', + overwriteName: '' +}; +UserProfile.propTypes = { + userId: React.PropTypes.string, + overwriteName: React.PropTypes.string +}; diff --git a/web/react/components/user_settings_appearance.jsx b/web/react/components/user_settings_appearance.jsx index ba2d97ea8..878ec42fc 100644 --- a/web/react/components/user_settings_appearance.jsx +++ b/web/react/components/user_settings_appearance.jsx @@ -4,100 +4,133 @@ var UserStore = require('../stores/user_store.jsx'); var SettingItemMin = require('./setting_item_min.jsx'); var SettingItemMax = require('./setting_item_max.jsx'); -var client = require('../utils/client.jsx'); -var utils = require('../utils/utils.jsx'); +var Client = require('../utils/client.jsx'); +var Utils = require('../utils/utils.jsx'); -module.exports = React.createClass({ - submitTheme: function(e) { +export default class UserSettingsAppearance extends React.Component { + constructor(props) { + super(props); + + this.submitTheme = this.submitTheme.bind(this); + this.updateTheme = this.updateTheme.bind(this); + this.handleClose = this.handleClose.bind(this); + + this.state = this.getStateFromStores(); + } + getStateFromStores() { + var user = UserStore.getCurrentUser(); + var theme = '#2389d7'; + if (config.ThemeColors != null) { + theme = config.ThemeColors[0]; + } + if (user.props && user.props.theme) { + theme = user.props.theme; + } + + return {theme: theme.toLowerCase()}; + } + submitTheme(e) { e.preventDefault(); var user = UserStore.getCurrentUser(); - if (!user.props) user.props = {}; + if (!user.props) { + user.props = {}; + } user.props.theme = this.state.theme; - client.updateUser(user, - function(data) { - this.props.updateSection(""); + Client.updateUser(user, + function success() { + this.props.updateSection(''); window.location.reload(); }.bind(this), - function(err) { - state = this.getInitialState(); - state.server_error = err; + function fail(err) { + var state = this.getStateFromStores(); + state.serverError = err; this.setState(state); }.bind(this) ); - }, - updateTheme: function(e) { - var hex = utils.rgb2hex(e.target.style.backgroundColor); - this.setState({ theme: hex.toLowerCase() }); - }, - handleClose: function() { - this.setState({server_error: null}); + } + updateTheme(e) { + var hex = Utils.rgb2hex(e.target.style.backgroundColor); + this.setState({theme: hex.toLowerCase()}); + } + handleClose() { + this.setState({serverError: null}); this.props.updateTab('general'); - }, - componentDidMount: function() { - if (this.props.activeSection === "theme") { - $(this.refs[this.state.theme].getDOMNode()).addClass('active-border'); + } + componentDidMount() { + if (this.props.activeSection === 'theme') { + $(React.findDOMNode(this.refs[this.state.theme])).addClass('active-border'); } $('#user_settings').on('hidden.bs.modal', this.handleClose); - }, - componentDidUpdate: function() { - if (this.props.activeSection === "theme") { + } + componentDidUpdate() { + if (this.props.activeSection === 'theme') { $('.color-btn').removeClass('active-border'); - $(this.refs[this.state.theme].getDOMNode()).addClass('active-border'); + $(React.findDOMNode(this.refs[this.state.theme])).addClass('active-border'); } - }, - componentWillUnmount: function() { + } + componentWillUnmount() { $('#user_settings').off('hidden.bs.modal', this.handleClose); this.props.updateSection(''); - }, - getInitialState: function() { - var user = UserStore.getCurrentUser(); - var theme = config.ThemeColors != null ? config.ThemeColors[0] : "#2389d7"; - if (user.props && user.props.theme) { - theme = user.props.theme; + } + render() { + var serverError; + if (this.state.serverError) { + serverError = this.state.serverError; } - return { theme: theme.toLowerCase() }; - }, - render: function() { - var server_error = this.state.server_error ? this.state.server_error : null; - var themeSection; var self = this; if (config.ThemeColors != null) { if (this.props.activeSection === 'theme') { - var theme_buttons = []; + var themeButtons = []; for (var i = 0; i < config.ThemeColors.length; i++) { - theme_buttons.push(<button ref={config.ThemeColors[i]} type="button" className="btn btn-lg color-btn" style={{backgroundColor: config.ThemeColors[i]}} onClick={this.updateTheme} />); + themeButtons.push( + <button + ref={config.ThemeColors[i]} + type='button' + className='btn btn-lg color-btn' + style={{backgroundColor: config.ThemeColors[i]}} + onClick={this.updateTheme} + /> + ); } var inputs = []; inputs.push( - <li className="setting-list-item"> - <div className="btn-group" data-toggle="buttons-radio"> - { theme_buttons } + <li className='setting-list-item'> + <div + className='btn-group' + data-toggle='buttons-radio' + > + {themeButtons} </div> </li> ); themeSection = ( <SettingItemMax - title="Theme Color" + title='Theme Color' inputs={inputs} submit={this.submitTheme} - server_error={server_error} - updateSection={function(e){self.props.updateSection("");e.preventDefault;}} + serverError={serverError} + updateSection={function updateSection(e) { + self.props.updateSection(''); + e.preventDefault(); + }} /> ); } else { themeSection = ( <SettingItemMin - title="Theme Color" + title='Theme Color' describe={this.state.theme} - updateSection={function(){self.props.updateSection("theme");}} + updateSection={function updateSection() { + self.props.updateSection('theme'); + }} /> ); } @@ -105,17 +138,38 @@ module.exports = React.createClass({ return ( <div> - <div className="modal-header"> - <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> - <h4 className="modal-title" ref="title"><i className="modal-back"></i>Appearance 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>Appearance Settings + </h4> </div> - <div className="user-settings"> - <h3 className="tab-header">Appearance Settings</h3> - <div className="divider-dark first"/> + <div className='user-settings'> + <h3 className='tab-header'>Appearance Settings</h3> + <div className='divider-dark first'/> {themeSection} - <div className="divider-dark"/> + <div className='divider-dark'/> </div> </div> ); } -}); +} + +UserSettingsAppearance.defaultProps = { + activeSection: '' +}; +UserSettingsAppearance.propTypes = { + activeSection: React.PropTypes.string, + updateSection: React.PropTypes.func, + updateTab: React.PropTypes.func +}; diff --git a/web/react/components/user_settings_security.jsx b/web/react/components/user_settings_security.jsx index ae8a5f0bc..a9f62097a 100644 --- a/web/react/components/user_settings_security.jsx +++ b/web/react/components/user_settings_security.jsx @@ -3,13 +3,23 @@ var SettingItemMin = require('./setting_item_min.jsx'); var SettingItemMax = require('./setting_item_max.jsx'); -var client = require('../utils/client.jsx'); +var Client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var Constants = require('../utils/constants.jsx'); -module.exports = React.createClass({ - displayName: 'SecurityTab', - submitPassword: function(e) { +export default class SecurityTab extends React.Component { + constructor(props) { + super(props); + + this.submitPassword = this.submitPassword.bind(this); + this.updateCurrentPassword = this.updateCurrentPassword.bind(this); + this.updateNewPassword = this.updateNewPassword.bind(this); + this.updateConfirmPassword = this.updateConfirmPassword.bind(this); + this.handleClose = this.handleClose.bind(this); + + this.state = {currentPassword: '', newPassword: '', confirmPassword: ''}; + } + submitPassword(e) { e.preventDefault(); var user = this.props.user; @@ -37,13 +47,13 @@ module.exports = React.createClass({ data.current_password = currentPassword; data.new_password = newPassword; - client.updatePassword(data, - function() { + Client.updatePassword(data, + function success() { this.props.updateSection(''); AsyncClient.getMe(); this.setState({currentPassword: '', newPassword: '', confirmPassword: ''}); }.bind(this), - function(err) { + function fail(err) { var state = this.getInitialState(); if (err.message) { state.serverError = err.message; @@ -54,47 +64,49 @@ module.exports = React.createClass({ this.setState(state); }.bind(this) ); - }, - updateCurrentPassword: function(e) { + } + updateCurrentPassword(e) { this.setState({currentPassword: e.target.value}); - }, - updateNewPassword: function(e) { + } + updateNewPassword(e) { this.setState({newPassword: e.target.value}); - }, - updateConfirmPassword: function(e) { + } + updateConfirmPassword(e) { this.setState({confirmPassword: e.target.value}); - }, - handleHistoryOpen: function() { - $("#user_settings").modal('hide'); - }, - handleDevicesOpen: function() { - $("#user_settings").modal('hide'); - }, - handleClose: function() { - $(this.getDOMNode()).find('.form-control').each(function() { + } + handleHistoryOpen() { + $('#user_settings').modal('hide'); + } + handleDevicesOpen() { + $('#user_settings').modal('hide'); + } + handleClose() { + $(React.findDOMNode(this)).find('.form-control').each(function resetValue() { this.value = ''; }); this.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null}); this.props.updateTab('general'); - }, - componentDidMount: function() { + } + componentDidMount() { $('#user_settings').on('hidden.bs.modal', this.handleClose); - }, - componentWillUnmount: function() { + } + componentWillUnmount() { $('#user_settings').off('hidden.bs.modal', this.handleClose); this.props.updateSection(''); - }, - getInitialState: function() { - return {currentPassword: '', newPassword: '', confirmPassword: ''}; - }, - render: function() { - var serverError = this.state.serverError ? this.state.serverError : null; - var passwordError = this.state.passwordError ? this.state.passwordError : null; + } + render() { + var serverError; + if (this.state.serverError) { + serverError = this.state.serverError; + } + var passwordError; + if (this.state.passwordError) { + passwordError = this.state.passwordError; + } var updateSectionStatus; var passwordSection; - var self = this; if (this.props.activeSection === 'password') { var inputs = []; var submit = null; @@ -104,7 +116,12 @@ module.exports = React.createClass({ <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}/> + <input + className='form-control' + type='password' + onChange={this.updateCurrentPassword} + value={this.state.currentPassword} + /> </div> </div> ); @@ -112,7 +129,12 @@ module.exports = React.createClass({ <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}/> + <input + className='form-control' + type='password' + onChange={this.updateNewPassword} + value={this.state.newPassword} + /> </div> </div> ); @@ -120,7 +142,12 @@ module.exports = React.createClass({ <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}/> + <input + className='form-control' + type='password' + onChange={this.updateConfirmPassword} + value={this.state.confirmPassword} + /> </div> </div> ); @@ -134,11 +161,11 @@ module.exports = React.createClass({ ); } - updateSectionStatus = function(e) { - self.props.updateSection(''); - self.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null}); + updateSectionStatus = function resetSection(e) { + this.props.updateSection(''); + this.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null}); e.preventDefault(); - }; + }.bind(this); passwordSection = ( <SettingItemMax @@ -154,17 +181,27 @@ module.exports = React.createClass({ var describe; 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'; + var hour = '12'; + if (d.getHours() % 12) { + hour = String(d.getHours() % 12); + } + var min = String(d.getMinutes()); + if (d.getMinutes() < 10) { + min = '0' + d.getMinutes(); + } + var timeOfDay = ' am'; + if (d.getHours() >= 12) { + timeOfDay = ' pm'; + } + describe = 'Last updated ' + Constants.MONTHS[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear() + ' at ' + hour + ':' + min + timeOfDay; } else { describe = 'Log in done through GitLab'; } - updateSectionStatus = function() { - self.props.updateSection('password'); - }; + updateSectionStatus = function updateSection() { + this.props.updateSection('password'); + }.bind(this); passwordSection = ( <SettingItemMin @@ -178,8 +215,20 @@ module.exports = React.createClass({ return ( <div> <div className='modal-header'> - <button type='button' className='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>×</span></button> - <h4 className='modal-title' ref='title'><i className='modal-back'></i>Security Settings</h4> + <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> @@ -187,11 +236,38 @@ module.exports = React.createClass({ {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> + <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='#activity-log' + href='#' + onClick={this.handleDevicesOpen} + > + <i className='fa fa-globe'></i>View and Logout of Active Sessions + </a> </div> </div> ); } -}); +} + +SecurityTab.defaultProps = { + user: {}, + activeSection: '' +}; +SecurityTab.propTypes = { + user: React.PropTypes.object, + activeSection: React.PropTypes.string, + updateSection: React.PropTypes.func, + updateTab: React.PropTypes.func +}; diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx index 24efce0f1..6e61ab156 100644 --- a/web/react/components/view_image.jsx +++ b/web/react/components/view_image.jsx @@ -2,35 +2,46 @@ // See License.txt for license information. var Client = require('../utils/client.jsx'); -var utils = require('../utils/utils.jsx'); +var Utils = require('../utils/utils.jsx'); -module.exports = React.createClass({ - displayName: 'ViewImageModal', - propTypes: { - filenames: React.PropTypes.array, - modalId: React.PropTypes.string, - channelId: React.PropTypes.string, - userId: React.PropTypes.string, - startId: React.PropTypes.number - }, - canSetState: false, - handleNext: function() { +export default class ViewImageModal extends React.Component { + constructor(props) { + super(props); + + this.canSetState = false; + + this.loadImage = this.loadImage.bind(this); + this.handleNext = this.handleNext.bind(this); + this.handlePrev = this.handlePrev.bind(this); + this.handleKeyPress = this.handleKeyPress.bind(this); + this.getPublicLink = this.getPublicLink.bind(this); + this.getPreviewImagePath = this.getPreviewImagePath.bind(this); + + var loaded = []; + var progress = []; + for (var i = 0; i < this.props.filenames.length; i++) { + loaded.push(false); + progress.push(0); + } + this.state = {imgId: this.props.startId, viewed: false, loaded: loaded, progress: progress, images: {}, fileSizes: {}}; + } + handleNext() { var id = this.state.imgId + 1; if (id > this.props.filenames.length - 1) { id = 0; } this.setState({imgId: id}); this.loadImage(id); - }, - handlePrev: function() { + } + handlePrev() { var id = this.state.imgId - 1; if (id < 0) { id = this.props.filenames.length - 1; } this.setState({imgId: id}); this.loadImage(id); - }, - handleKeyPress: function handleKeyPress(e) { + } + handleKeyPress(e) { if (!e) { return; } else if (e.keyCode === 39) { @@ -38,11 +49,11 @@ module.exports = React.createClass({ } else if (e.keyCode === 37) { this.handlePrev(); } - }, - componentWillReceiveProps: function(nextProps) { + } + componentWillReceiveProps(nextProps) { this.setState({imgId: nextProps.startId}); - }, - loadImage: function(id) { + } + loadImage(id) { var imgHeight = $(window).height() - 100; if (this.state.loaded[id] || this.state.images[id]) { $('.modal .modal-image .image-wrapper img').css('max-height', imgHeight); @@ -51,26 +62,25 @@ module.exports = React.createClass({ var filename = this.props.filenames[id]; - var fileInfo = utils.splitFileLocation(filename); - var fileType = utils.getFileType(fileInfo.ext); + var fileInfo = Utils.splitFileLocation(filename); + var fileType = Utils.getFileType(fileInfo.ext); if (fileType === 'image') { - var self = this; var img = new Image(); img.load(this.getPreviewImagePath(filename), - function() { - var progress = self.state.progress; + function load() { + var progress = this.state.progress; progress[id] = img.completedPercentage; - self.setState({progress: progress}); - }); - img.onload = function onload(imgid) { + this.setState({progress: progress}); + }.bind(this)); + img.onload = (function onload(imgid) { return function onloadReturn() { - var loaded = self.state.loaded; + var loaded = this.state.loaded; loaded[imgid] = true; - self.setState({loaded: loaded}); - $(self.refs.image.getDOMNode()).css('max-height', imgHeight); - }; - }(id); + this.setState({loaded: loaded}); + $(React.findDOMNode(this.refs.image)).css('max-height', imgHeight); + }.bind(this); + }.bind(this)(id)); var images = this.state.images; images[id] = img; this.setState({images: images}); @@ -80,52 +90,51 @@ module.exports = React.createClass({ loaded[id] = true; this.setState({loaded: loaded}); } - }, - componentDidUpdate: function() { + } + componentDidUpdate() { if (this.state.loaded[this.state.imgId]) { if (this.refs.imageWrap) { - $(this.refs.imageWrap.getDOMNode()).removeClass('default'); + $(React.findDOMNode(this.refs.imageWrap)).removeClass('default'); } } - }, - componentDidMount: function() { - var self = this; + } + componentDidMount() { $('#' + this.props.modalId).on('shown.bs.modal', function onModalShow() { - self.setState({viewed: true}); - self.loadImage(self.state.imgId); - }); + this.setState({viewed: true}); + this.loadImage(this.state.imgId); + }.bind(this)); - $(this.refs.modal.getDOMNode()).click(function onModalClick(e) { - if (e.target === this || e.target === self.refs.imageBody.getDOMNode()) { + $(React.findDOMNode(this.refs.modal)).click(function onModalClick(e) { + if (e.target === this || e.target === React.findDOMNode(this.refs.imageBody)) { $('.image_modal').modal('hide'); } - }); + }.bind(this)); - $(this.refs.imageWrap.getDOMNode()).hover( + $(React.findDOMNode(this.refs.imageWrap)).hover( function onModalHover() { - $(self.refs.imageFooter.getDOMNode()).addClass('footer--show'); - }, function offModalHover() { - $(self.refs.imageFooter.getDOMNode()).removeClass('footer--show'); - } + $(React.findDOMNode(this.refs.imageFooter)).addClass('footer--show'); + }.bind(this), function offModalHover() { + $(React.findDOMNode(this.refs.imageFooter)).removeClass('footer--show'); + }.bind(this) ); if (this.refs.previewArrowLeft) { - $(this.refs.previewArrowLeft.getDOMNode()).hover( + $(React.findDOMNode(this.refs.previewArrowLeft)).hover( function onModalHover() { - $(self.refs.imageFooter.getDOMNode()).addClass('footer--show'); - }, function offModalHover() { - $(self.refs.imageFooter.getDOMNode()).removeClass('footer--show'); - } + $(React.findDOMNode(this.refs.imageFooter)).addClass('footer--show'); + }.bind(this), function offModalHover() { + $(React.findDOMNode(this.refs.imageFooter)).removeClass('footer--show'); + }.bind(this) ); } if (this.refs.previewArrowRight) { - $(this.refs.previewArrowRight.getDOMNode()).hover( + $(React.findDOMNode(this.refs.previewArrowRight)).hover( function onModalHover() { - $(self.refs.imageFooter.getDOMNode()).addClass('footer--show'); - }, function offModalHover() { - $(self.refs.imageFooter.getDOMNode()).removeClass('footer--show'); - } + $(React.findDOMNode(this.refs.imageFooter)).addClass('footer--show'); + }.bind(this), function offModalHover() { + $(React.findDOMNode(this.refs.imageFooter)).removeClass('footer--show'); + }.bind(this) ); } @@ -133,90 +142,93 @@ module.exports = React.createClass({ // keep track of whether or not this component is mounted so we can safely set the state asynchronously this.canSetState = true; - }, - componentWillUnmount: function() { + } + componentWillUnmount() { this.canSetState = false; $(window).off('keyup', this.handleKeyPress); - }, - getPublicLink: function() { + } + getPublicLink() { var data = {}; data.channel_id = this.props.channelId; data.user_id = this.props.userId; data.filename = this.props.filenames[this.state.imgId]; Client.getPublicLink(data, function sucess(serverData) { - if (utils.isMobile()) { + if (Utils.isMobile()) { window.location.href = serverData.public_link; } else { window.open(serverData.public_link); } }, - function error() { - } + function error() {} ); - }, - getPreviewImagePath: function(filename) { + } + getPreviewImagePath(filename) { // Returns the path to a preview image that can be used to represent a file. - var fileInfo = utils.splitFileLocation(filename); - var fileType = utils.getFileType(fileInfo.ext); + var fileInfo = Utils.splitFileLocation(filename); + var fileType = Utils.getFileType(fileInfo.ext); if (fileType === 'image') { // This is a temporary patch to fix issue with old files using absolute paths if (fileInfo.path.indexOf('/api/v1/files/get') !== -1) { fileInfo.path = fileInfo.path.split('/api/v1/files/get')[1]; } - fileInfo.path = utils.getWindowLocationOrigin() + '/api/v1/files/get' + fileInfo.path; + fileInfo.path = Utils.getWindowLocationOrigin() + '/api/v1/files/get' + fileInfo.path; return fileInfo.path + '_preview.jpg'; } // only images have proper previews, so just use a placeholder icon for non-images - return utils.getPreviewImagePathForFileType(fileType); - }, - getInitialState: function() { - var loaded = []; - var progress = []; - for (var i = 0; i < this.props.filenames.length; i++) { - loaded.push(false); - progress.push(0); - } - return {imgId: this.props.startId, viewed: false, loaded: loaded, progress: progress, images: {}, fileSizes: {}}; - }, - render: function() { + return Utils.getPreviewImagePathForFileType(fileType); + } + render() { if (this.props.filenames.length < 1 || this.props.filenames.length - 1 < this.state.imgId) { return <div/>; } var filename = this.props.filenames[this.state.imgId]; - var fileUrl = utils.getFileUrl(filename); + var fileUrl = Utils.getFileUrl(filename); - var name = decodeURIComponent(utils.getFileName(filename)); + var name = decodeURIComponent(Utils.getFileName(filename)); var content; var bgClass = ''; if (this.state.loaded[this.state.imgId]) { - var fileInfo = utils.splitFileLocation(filename); - var fileType = utils.getFileType(fileInfo.ext); + var fileInfo = Utils.splitFileLocation(filename); + var fileType = Utils.getFileType(fileInfo.ext); if (fileType === 'image') { // image files just show a preview of the file content = ( - <a href={fileUrl} target='_blank'> - <img ref='image' src={this.getPreviewImagePath(filename)}/> + <a + href={fileUrl} + target='_blank' + > + <img + ref='image' + src={this.getPreviewImagePath(filename)} + /> </a> ); } else { // non-image files include a section providing details about the file var infoString = 'File type ' + fileInfo.ext.toUpperCase(); if (this.state.fileSizes[filename] && this.state.fileSizes[filename] >= 0) { - infoString += ', Size ' + utils.fileSizeToString(this.state.fileSizes[filename]); + infoString += ', Size ' + Utils.fileSizeToString(this.state.fileSizes[filename]); } content = ( <div className='file-details__container'> - <a className={'file-details__preview'} href={fileUrl} target='_blank'> + <a + className={'file-details__preview'} + href={fileUrl} + target='_blank' + > <span className='file-details__preview-helper' /> - <img ref='image' src={this.getPreviewImagePath(filename)} /> + <img + ref='image' + src={this.getPreviewImagePath(filename)} + /> </a> <div className='file-details'> <div className='file-details__name'>{name}</div> @@ -228,19 +240,16 @@ module.exports = React.createClass({ // asynchronously request the actual size of this file if (!(filename in this.state.fileSizes)) { - var self = this; - Client.getFileInfo( filename, - function(data) { - if (self.canSetState) { - var fileSizes = self.state.fileSizes; - fileSizes[filename] = parseInt(data["size"], 10); - self.setState(fileSizes); + function success(data) { + if (this.canSetState) { + var fileSizes = this.state.fileSizes; + fileSizes[filename] = parseInt(data.size, 10); + this.setState(fileSizes); } - }, - function(err) { - } + }.bind(this), + function fail() {} ); } } @@ -250,14 +259,22 @@ module.exports = React.createClass({ if (percentage) { content = ( <div> - <img className='loader-image' src='/static/images/load.gif' /> - <span className='loader-percent' >{'Previewing ' + percentage + '%'}</span> + <img + className='loader-image' + src='/static/images/load.gif' + /> + <span className='loader-percent'> + {'Previewing ' + percentage + '%'} + </span> </div> ); } else { content = ( <div> - <img className='loader-image' src='/static/images/load.gif' /> + <img + className='loader-image' + src='/static/images/load.gif' + /> </div> ); } @@ -268,7 +285,14 @@ module.exports = React.createClass({ if (config.AllowPublicLink) { publicLink = ( <div> - <a href='#' className='public-link text' data-title='Public Image' onClick={this.getPublicLink}>Get Public Link</a> + <a + href='#' + className='public-link text' + data-title='Public Image' + onClick={this.getPublicLink} + > + Get Public Link + </a> <span className='text'> | </span> </div> ); @@ -299,18 +323,43 @@ module.exports = React.createClass({ } return ( - <div className='modal fade image_modal' ref='modal' id={this.props.modalId} tabIndex='-1' role='dialog' aria-hidden='true'> + <div + className='modal fade image_modal' + ref='modal' + id={this.props.modalId} + tabIndex='-1' + role='dialog' + aria-hidden='true' + > <div className='modal-dialog modal-image'> <div className='modal-content image-content'> - <div ref='imageBody' className='modal-body image-body'> - <div ref='imageWrap' className={'image-wrapper default ' + bgClass}> - <div className='modal-close' data-dismiss='modal'></div> + <div + ref='imageBody' + className='modal-body image-body' + > + <div + ref='imageWrap' + className={'image-wrapper default ' + bgClass} + > + <div + className='modal-close' + data-dismiss='modal' + /> {content} - <div ref='imageFooter' className='modal-button-bar'> + <div + ref='imageFooter' + className='modal-button-bar' + > <span className='pull-left text'>{'File ' + (this.state.imgId + 1) + ' of ' + this.props.filenames.length}</span> <div className='image-links'> {publicLink} - <a href={fileUrl} download={name} className='text'>Download</a> + <a + href={fileUrl} + download={name} + className='text' + > + Download + </a> </div> </div> </div> @@ -322,4 +371,19 @@ module.exports = React.createClass({ </div> ); } -}); +} + +ViewImageModal.defaultProps = { + filenames: [], + modalId: '', + channelId: '', + userId: '', + startId: 0 +}; +ViewImageModal.propTypes = { + filenames: React.PropTypes.array, + modalId: React.PropTypes.string, + channelId: React.PropTypes.string, + userId: React.PropTypes.string, + startId: React.PropTypes.number +}; |