diff options
Diffstat (limited to 'web/react')
33 files changed, 1304 insertions, 373 deletions
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index 6fddfef07..92f0bbdce 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -3,6 +3,7 @@ var AdminSidebar = require('./admin_sidebar.jsx'); var AdminStore = require('../../stores/admin_store.jsx'); +var TeamStore = require('../../stores/team_store.jsx'); var AsyncClient = require('../../utils/async_client.jsx'); var LoadingScreen = require('../loading_screen.jsx'); @@ -16,38 +17,104 @@ var GitLabSettingsTab = require('./gitlab_settings.jsx'); var SqlSettingsTab = require('./sql_settings.jsx'); var TeamSettingsTab = require('./team_settings.jsx'); var ServiceSettingsTab = require('./service_settings.jsx'); +var TeamUsersTab = require('./team_users.jsx'); export default class AdminController extends React.Component { constructor(props) { super(props); this.selectTab = this.selectTab.bind(this); + this.removeSelectedTeam = this.removeSelectedTeam.bind(this); + this.addSelectedTeam = this.addSelectedTeam.bind(this); this.onConfigListenerChange = this.onConfigListenerChange.bind(this); + this.onAllTeamsListenerChange = this.onAllTeamsListenerChange.bind(this); + + var selectedTeams = AdminStore.getSelectedTeams(); + if (selectedTeams == null) { + selectedTeams = {}; + selectedTeams[TeamStore.getCurrentId()] = 'true'; + AdminStore.saveSelectedTeams(selectedTeams); + } this.state = { - config: null, - selected: 'service_settings' + config: AdminStore.getConfig(), + teams: AdminStore.getAllTeams(), + selectedTeams, + selected: 'service_settings', + selectedTeam: null }; } componentDidMount() { AdminStore.addConfigChangeListener(this.onConfigListenerChange); AsyncClient.getConfig(); + + AdminStore.addAllTeamsChangeListener(this.onAllTeamsListenerChange); + AsyncClient.getAllTeams(); } componentWillUnmount() { AdminStore.removeConfigChangeListener(this.onConfigListenerChange); + AdminStore.removeAllTeamsChangeListener(this.onAllTeamsListenerChange); } onConfigListenerChange() { this.setState({ config: AdminStore.getConfig(), - selected: this.state.selected + teams: AdminStore.getAllTeams(), + selectedTeams: AdminStore.getSelectedTeams(), + selected: this.state.selected, + selectedTeam: this.state.selectedTeam }); } - selectTab(tab) { - this.setState({selected: tab}); + onAllTeamsListenerChange() { + this.setState({ + config: AdminStore.getConfig(), + teams: AdminStore.getAllTeams(), + selectedTeams: AdminStore.getSelectedTeams(), + selected: this.state.selected, + selectedTeam: this.state.selectedTeam + + }); + } + + selectTab(tab, teamId) { + this.setState({ + config: AdminStore.getConfig(), + teams: AdminStore.getAllTeams(), + selectedTeams: AdminStore.getSelectedTeams(), + selected: tab, + selectedTeam: teamId + }); + } + + removeSelectedTeam(teamId) { + var selectedTeams = AdminStore.getSelectedTeams(); + Reflect.deleteProperty(selectedTeams, teamId); + AdminStore.saveSelectedTeams(selectedTeams); + + this.setState({ + config: AdminStore.getConfig(), + teams: AdminStore.getAllTeams(), + selectedTeams: AdminStore.getSelectedTeams(), + selected: this.state.selected, + selectedTeam: this.state.selectedTeam + }); + } + + addSelectedTeam(teamId) { + var selectedTeams = AdminStore.getSelectedTeams(); + selectedTeams[teamId] = 'true'; + AdminStore.saveSelectedTeams(selectedTeams); + + this.setState({ + config: AdminStore.getConfig(), + teams: AdminStore.getAllTeams(), + selectedTeams: AdminStore.getSelectedTeams(), + selected: this.state.selected, + selectedTeam: this.state.selectedTeam + }); } render() { @@ -74,6 +141,8 @@ export default class AdminController extends React.Component { tab = <TeamSettingsTab config={this.state.config} />; } else if (this.state.selected === 'service_settings') { tab = <ServiceSettingsTab config={this.state.config} />; + } else if (this.state.selected === 'team_users') { + tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />; } } @@ -85,7 +154,12 @@ export default class AdminController extends React.Component { /> <AdminSidebar selected={this.state.selected} + selectedTeam={this.state.selectedTeam} selectTab={this.selectTab} + teams={this.state.teams} + selectedTeams={this.state.selectedTeams} + removeSelectedTeam={this.removeSelectedTeam} + addSelectedTeam={this.addSelectedTeam} /> <div className='inner__wrap channel__wrap'> <div className='row header'> diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index 17ce39c7c..cebb3ff20 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -2,6 +2,7 @@ // See License.txt for license information. var AdminSidebarHeader = require('./admin_sidebar_header.jsx'); +var SelectTeamModal = require('./select_team_modal.jsx'); export default class AdminSidebar extends React.Component { constructor(props) { @@ -9,28 +10,121 @@ export default class AdminSidebar extends React.Component { this.isSelected = this.isSelected.bind(this); this.handleClick = this.handleClick.bind(this); + this.removeTeam = this.removeTeam.bind(this); + + this.showTeamSelect = this.showTeamSelect.bind(this); + this.teamSelectedModal = this.teamSelectedModal.bind(this); + this.teamSelectedModalDismissed = this.teamSelectedModalDismissed.bind(this); this.state = { + showSelectModal: false }; } - handleClick(name, e) { + handleClick(name, teamId, e) { e.preventDefault(); - this.props.selectTab(name); + this.props.selectTab(name, teamId); } - isSelected(name) { + isSelected(name, teamId) { if (this.props.selected === name) { - return 'active'; + if (name === 'team_users') { + if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) { + return 'active'; + } + } else { + return 'active'; + } } return ''; } + removeTeam(teamId, e) { + e.preventDefault(); + Reflect.deleteProperty(this.props.selectedTeams, teamId); + this.props.removeSelectedTeam(teamId); + + if (this.props.selected === 'team_users') { + if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) { + this.props.selectTab('service_settings', null); + } + } + } + componentDidMount() { } + showTeamSelect(e) { + e.preventDefault(); + this.setState({showSelectModal: true}); + } + + teamSelectedModal(teamId) { + this.props.selectedTeams[teamId] = 'true'; + this.setState({showSelectModal: false}); + this.props.addSelectedTeam(teamId); + this.forceUpdate(); + } + + teamSelectedModalDismissed() { + this.setState({showSelectModal: false}); + } + render() { + var count = '*'; + var teams = 'Loading'; + + if (this.props.teams != null) { + count = '' + Object.keys(this.props.teams).length; + + teams = []; + for (var key in this.props.selectedTeams) { + if (this.props.selectedTeams.hasOwnProperty(key)) { + var team = this.props.teams[key]; + + if (team != null) { + teams.push( + <ul + key={'team_' + team.id} + className='nav nav__sub-menu' + > + <li> + <a + href='#' + onClick={this.handleClick.bind(this, 'team_users', team.id)} + className={'nav__sub-menu-item ' + this.isSelected('team_users', team.id)} + > + {team.name} + <span + className='menu-icon--right menu__close' + onClick={this.removeTeam.bind(this, team.id)} + style={{cursor: 'pointer'}} + > + {'x'} + </span> + </a> + </li> + <li> + <ul className='nav nav__inner-menu'> + <li> + <a + href='#' + className={this.isSelected('team_users', team.id)} + onClick={this.handleClick.bind(this, 'team_users', team.id)} + > + {'- Users'} + </a> + </li> + </ul> + </li> + </ul> + ); + } + } + } + } + return ( <div className='sidebar--left sidebar--collapsable'> <div> @@ -39,10 +133,16 @@ export default class AdminSidebar extends React.Component { <li> <ul className='nav nav__sub-menu'> <li> + <h4> + <span className='icon fa fa-gear'></span> + <span>{'SETTINGS'}</span> + </h4> + </li> + <li> <a href='#' className={this.isSelected('service_settings')} - onClick={this.handleClick.bind(this, 'service_settings')} + onClick={this.handleClick.bind(this, 'service_settings', null)} > {'Service Settings'} </a> @@ -51,7 +151,7 @@ export default class AdminSidebar extends React.Component { <a href='#' className={this.isSelected('team_settings')} - onClick={this.handleClick.bind(this, 'team_settings')} + onClick={this.handleClick.bind(this, 'team_settings', null)} > {'Team Settings'} </a> @@ -60,7 +160,7 @@ export default class AdminSidebar extends React.Component { <a href='#' className={this.isSelected('sql_settings')} - onClick={this.handleClick.bind(this, 'sql_settings')} + onClick={this.handleClick.bind(this, 'sql_settings', null)} > {'SQL Settings'} </a> @@ -69,7 +169,7 @@ export default class AdminSidebar extends React.Component { <a href='#' className={this.isSelected('email_settings')} - onClick={this.handleClick.bind(this, 'email_settings')} + onClick={this.handleClick.bind(this, 'email_settings', null)} > {'Email Settings'} </a> @@ -78,7 +178,7 @@ export default class AdminSidebar extends React.Component { <a href='#' className={this.isSelected('image_settings')} - onClick={this.handleClick.bind(this, 'image_settings')} + onClick={this.handleClick.bind(this, 'image_settings', null)} > {'File Settings'} </a> @@ -87,7 +187,7 @@ export default class AdminSidebar extends React.Component { <a href='#' className={this.isSelected('log_settings')} - onClick={this.handleClick.bind(this, 'log_settings')} + onClick={this.handleClick.bind(this, 'log_settings', null)} > {'Log Settings'} </a> @@ -95,17 +195,8 @@ export default class AdminSidebar extends React.Component { <li> <a href='#' - className={this.isSelected('logs')} - onClick={this.handleClick.bind(this, 'logs')} - > - {'Logs'} - </a> - </li> - <li> - <a - href='#' className={this.isSelected('rate_settings')} - onClick={this.handleClick.bind(this, 'rate_settings')} + onClick={this.handleClick.bind(this, 'rate_settings', null)} > {'Rate Limit Settings'} </a> @@ -114,7 +205,7 @@ export default class AdminSidebar extends React.Component { <a href='#' className={this.isSelected('privacy_settings')} - onClick={this.handleClick.bind(this, 'privacy_settings')} + onClick={this.handleClick.bind(this, 'privacy_settings', null)} > {'Privacy Settings'} </a> @@ -123,21 +214,65 @@ export default class AdminSidebar extends React.Component { <a href='#' className={this.isSelected('gitlab_settings')} - onClick={this.handleClick.bind(this, 'gitlab_settings')} + onClick={this.handleClick.bind(this, 'gitlab_settings', null)} > {'GitLab Settings'} </a> </li> + <li> + <h4> + <span className='icon fa fa-gear'></span> + <span>{'TEAMS (' + count + ')'}</span> + <span className='menu-icon--right'> + <a + href='#' + onClick={this.showTeamSelect} + > + <i className='fa fa-plus'></i> + </a> + </span> + </h4> + </li> + <li> + {teams} + </li> + <li> + <h4> + <span className='icon fa fa-gear'></span> + <span>{'OTHER'}</span> + </h4> + </li> + <li> + <a + href='#' + className={this.isSelected('logs')} + onClick={this.handleClick.bind(this, 'logs', null)} + > + {'Logs'} + </a> + </li> </ul> </li> </ul> </div> + + <SelectTeamModal + teams={this.props.teams} + show={this.state.showSelectModal} + onModalSubmit={this.teamSelectedModal} + onModalDismissed={this.teamSelectedModalDismissed} + /> </div> ); } } AdminSidebar.propTypes = { + teams: React.PropTypes.object, + selectedTeams: React.PropTypes.object, + removeSelectedTeam: React.PropTypes.func, + addSelectedTeam: React.PropTypes.func, selected: React.PropTypes.string, + selectedTeam: React.PropTypes.string, selectTab: React.PropTypes.func };
\ No newline at end of file diff --git a/web/react/components/admin_console/reset_password_modal.jsx b/web/react/components/admin_console/reset_password_modal.jsx new file mode 100644 index 000000000..0b83edb17 --- /dev/null +++ b/web/react/components/admin_console/reset_password_modal.jsx @@ -0,0 +1,132 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var Client = require('../../utils/client.jsx'); +var Modal = ReactBootstrap.Modal; + +export default class ResetPasswordModal extends React.Component { + constructor(props) { + super(props); + + this.doSubmit = this.doSubmit.bind(this); + this.doCancel = this.doCancel.bind(this); + + this.state = { + serverError: null + }; + } + + doSubmit(e) { + e.preventDefault(); + var password = React.findDOMNode(this.refs.password).value; + + if (!password || password.length < 5) { + this.setState({serverError: 'Please enter at least 5 characters.'}); + return; + } + + this.setState({serverError: null}); + + var data = {}; + data.new_password = password; + data.name = this.props.team.name; + data.user_id = this.props.user.id; + + Client.resetPassword(data, + () => { + this.props.onModalSubmit(React.findDOMNode(this.refs.password).value); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + + doCancel() { + this.setState({serverError: null}); + this.props.onModalDismissed(); + } + + render() { + if (this.props.user == null) { + return <div/>; + } + + let urlClass = 'input-group input-group--limit'; + let serverError = null; + + if (this.state.serverError) { + urlClass += ' has-error'; + serverError = <div className='form-group has-error'><p className='input__help error'>{this.state.serverError}</p></div>; + } + + return ( + <Modal + show={this.props.show} + onHide={this.doCancel} + > + <Modal.Header closeButton={true}> + <Modal.Title>{'Reset Password'}</Modal.Title> + </Modal.Header> + <form + role='form' + className='form-horizontal' + > + <Modal.Body> + <div className='form-group'> + <div className='col-sm-10'> + <div className={urlClass}> + <span + data-toggle='tooltip' + title='New Password' + className='input-group-addon' + > + {'New Password'} + </span> + <input + type='password' + ref='password' + className='form-control' + maxLength='22' + autoFocus={true} + tabIndex='1' + /> + </div> + {serverError} + </div> + </div> + </Modal.Body> + <Modal.Footer> + <button + type='button' + className='btn btn-default' + onClick={this.doCancel} + > + {'Close'} + </button> + <button + onClick={this.doSubmit} + type='submit' + className='btn btn-primary' + tabIndex='2' + > + {'Select'} + </button> + </Modal.Footer> + </form> + </Modal> + ); + } +} + +ResetPasswordModal.defaultProps = { + show: false +}; + +ResetPasswordModal.propTypes = { + user: React.PropTypes.object, + team: React.PropTypes.object, + show: React.PropTypes.bool.isRequired, + onModalSubmit: React.PropTypes.func, + onModalDismissed: React.PropTypes.func +}; diff --git a/web/react/components/admin_console/select_team_modal.jsx b/web/react/components/admin_console/select_team_modal.jsx index fa30de7b2..343f65131 100644 --- a/web/react/components/admin_console/select_team_modal.jsx +++ b/web/react/components/admin_console/select_team_modal.jsx @@ -1,124 +1,99 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -export default class SelectTeam extends React.Component { +var Modal = ReactBootstrap.Modal; + +export default class SelectTeamModal extends React.Component { constructor(props) { super(props); - this.state = { - }; + this.doSubmit = this.doSubmit.bind(this); + this.doCancel = this.doCancel.bind(this); } + doSubmit(e) { + e.preventDefault(); + this.props.onModalSubmit(React.findDOMNode(this.refs.team).value); + } + doCancel() { + this.props.onModalDismissed(); + } render() { + if (this.props.teams == null) { + return <div/>; + } + + var options = []; + + for (var key in this.props.teams) { + if (this.props.teams.hasOwnProperty(key)) { + var team = this.props.teams[key]; + options.push( + <option + key={'opt_' + team.id} + value={team.id} + > + {team.name} + </option> + ); + } + } + return ( - <div className='modal fade' - id='select-team' - tabIndex='-1' - role='dialog' - aria-labelledby='teamsModalLabel' + <Modal + show={this.props.show} + onHide={this.doCancel} > - <div className='modal-dialog' - role='document' + <Modal.Header closeButton={true}> + <Modal.Title>{'Select Team'}</Modal.Title> + </Modal.Header> + <form + role='form' + className='form-horizontal' > - <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='teamsModalLabel' - > - {'Select a team'} - </h4> - </div> - <div className='modal-body'> - <table className='more-channel-table table'> - <tbody> - <tr> - <td> - <p className='more-channel-name'>{'Descartes'}</p> - </td> - <td className='td--action'> - <button className='btn btn-primary'>{'Join'}</button> - </td> - </tr> - <tr> - <td> - <p className='more-channel-name'>{'Grouping'}</p> - </td> - <td className='td--action'> - <button className='btn btn-primary'>{'Join'}</button> - </td> - </tr> - <tr> - <td> - <p className='more-channel-name'>{'Adventure'}</p> - </td> - <td className='td--action'> - <button className='btn btn-primary'>{'Join'}</button> - </td> - </tr> - <tr> - <td> - <p className='more-channel-name'>{'Crossroads'}</p> - </td> - <td className='td--action'> - <button className='btn btn-primary'>{'Join'}</button> - </td> - </tr> - <tr> - <td> - <p className='more-channel-name'>{'Sky scraping'}</p> - </td> - <td className='td--action'> - <button className='btn btn-primary'>{'Join'}</button> - </td> - </tr> - <tr> - <td> - <p className='more-channel-name'>{'Outdoors'}</p> - </td> - <td className='td--action'> - <button className='btn btn-primary'>{'Join'}</button> - </td> - </tr> - <tr> - <td> - <p className='more-channel-name'>{'Microsoft'}</p> - </td> - <td className='td--action'> - <button className='btn btn-primary'>{'Join'}</button> - </td> - </tr> - <tr> - <td> - <p className='more-channel-name'>{'Apple'}</p> - </td> - <td className='td--action'> - <button className='btn btn-primary'>{'Join'}</button> - </td> - </tr> - </tbody> - </table> - </div> - <div className='modal-footer'> - <button - type='button' - className='btn btn-default' - data-dismiss='modal' - > - {'Close'} - </button> + <Modal.Body> + <div className='form-group'> + <div className='col-sm-12'> + <select + ref='team' + size='10' + style={{width: '100%'}} + > + {options} + </select> + </div> </div> - </div> - </div> - </div> + </Modal.Body> + <Modal.Footer> + <button + type='button' + className='btn btn-default' + onClick={this.doCancel} + > + {'Close'} + </button> + <button + onClick={this.doSubmit} + type='submit' + className='btn btn-primary' + tabIndex='2' + > + {'Select'} + </button> + </Modal.Footer> + </form> + </Modal> ); } -}
\ No newline at end of file +} + +SelectTeamModal.defaultProps = { + show: false +}; + +SelectTeamModal.propTypes = { + teams: React.PropTypes.object, + show: React.PropTypes.bool.isRequired, + onModalSubmit: React.PropTypes.func, + onModalDismissed: React.PropTypes.func +}; diff --git a/web/react/components/admin_console/team_users.jsx b/web/react/components/admin_console/team_users.jsx new file mode 100644 index 000000000..0a971ff15 --- /dev/null +++ b/web/react/components/admin_console/team_users.jsx @@ -0,0 +1,178 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var Client = require('../../utils/client.jsx'); +var LoadingScreen = require('../loading_screen.jsx'); +var UserItem = require('./user_item.jsx'); +var ResetPasswordModal = require('./reset_password_modal.jsx'); + +export default class UserList extends React.Component { + constructor(props) { + super(props); + + this.getTeamProfiles = this.getTeamProfiles.bind(this); + this.getCurrentTeamProfiles = this.getCurrentTeamProfiles.bind(this); + this.doPasswordReset = this.doPasswordReset.bind(this); + this.doPasswordResetDismiss = this.doPasswordResetDismiss.bind(this); + this.doPasswordResetSubmit = this.doPasswordResetSubmit.bind(this); + + this.state = { + teamId: props.team.id, + users: null, + serverError: null, + showPasswordModal: false, + user: null + }; + } + + componentDidMount() { + this.getCurrentTeamProfiles(); + } + + getCurrentTeamProfiles() { + this.getTeamProfiles(this.props.team.id); + } + + // this.setState({ + // teamId: this.state.teamId, + // users: this.state.users, + // serverError: this.state.serverError, + // showPasswordModal: this.state.showPasswordModal, + // user: this.state.user + // }); + + getTeamProfiles(teamId) { + Client.getProfilesForTeam( + teamId, + (users) => { + var memberList = []; + for (var id in users) { + if (users.hasOwnProperty(id)) { + memberList.push(users[id]); + } + } + + memberList.sort((a, b) => { + if (a.username < b.username) { + return -1; + } + + if (a.username > b.username) { + return 1; + } + + return 0; + }); + + this.setState({ + teamId: this.state.teamId, + users: memberList, + serverError: this.state.serverError, + showPasswordModal: this.state.showPasswordModal, + user: this.state.user + }); + }, + (err) => { + this.setState({ + teamId: this.state.teamId, + users: null, + serverError: err.message, + showPasswordModal: this.state.showPasswordModal, + user: this.state.user + }); + } + ); + } + + doPasswordReset(user) { + this.setState({ + teamId: this.state.teamId, + users: this.state.users, + serverError: this.state.serverError, + showPasswordModal: true, + user + }); + } + + doPasswordResetDismiss() { + this.state.showPasswordModal = false; + this.state.user = null; + this.setState({ + teamId: this.state.teamId, + users: this.state.users, + serverError: this.state.serverError, + showPasswordModal: false, + user: null + }); + } + + doPasswordResetSubmit() { + this.setState({ + teamId: this.state.teamId, + users: this.state.users, + serverError: this.state.serverError, + showPasswordModal: false, + user: null + }); + } + + componentWillReceiveProps(newProps) { + this.getTeamProfiles(newProps.team.id); + } + + componentWillUnmount() { + } + + render() { + var serverError = ''; + if (this.state.serverError) { + serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>; + } + + if (this.state.users == null) { + return ( + <div className='wrapper--fixed'> + <h3>{'Users for ' + this.props.team.name}</h3> + {serverError} + <LoadingScreen /> + </div> + ); + } + + var memberList = this.state.users.map((user) => { + return ( + <UserItem + key={'user_' + user.id} + user={user} + refreshProfiles={this.getCurrentTeamProfiles} + doPasswordReset={this.doPasswordReset} + />); + }); + + return ( + <div className='wrapper--fixed'> + <h3>{'Users for ' + this.props.team.name + ' (' + this.state.users.length + ')'}</h3> + {serverError} + <form + className='form-horizontal' + role='form' + > + <div className='member-list-holder'> + {memberList} + </div> + </form> + <ResetPasswordModal + user={this.state.user} + show={this.state.showPasswordModal} + team={this.props.team} + onModalSubmit={this.doPasswordResetSubmit} + onModalDismissed={this.doPasswordResetDismiss} + /> + </div> + ); + } +} + +UserList.propTypes = { + team: React.PropTypes.object +}; diff --git a/web/react/components/admin_console/user_item.jsx b/web/react/components/admin_console/user_item.jsx new file mode 100644 index 000000000..32812e875 --- /dev/null +++ b/web/react/components/admin_console/user_item.jsx @@ -0,0 +1,266 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var Client = require('../../utils/client.jsx'); +var Utils = require('../../utils/utils.jsx'); + +export default class UserItem extends React.Component { + constructor(props) { + super(props); + + this.handleMakeMember = this.handleMakeMember.bind(this); + this.handleMakeActive = this.handleMakeActive.bind(this); + this.handleMakeNotActive = this.handleMakeNotActive.bind(this); + this.handleMakeAdmin = this.handleMakeAdmin.bind(this); + this.handleMakeSystemAdmin = this.handleMakeSystemAdmin.bind(this); + this.handleResetPassword = this.handleResetPassword.bind(this); + + this.state = {}; + } + + handleMakeMember(e) { + e.preventDefault(); + const data = { + user_id: this.props.user.id, + new_roles: '' + }; + + Client.updateRoles(data, + () => { + this.props.refreshProfiles(); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + + handleMakeActive(e) { + e.preventDefault(); + Client.updateActive(this.props.user.id, true, + () => { + this.props.refreshProfiles(); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + + handleMakeNotActive(e) { + e.preventDefault(); + Client.updateActive(this.props.user.id, false, + () => { + this.props.refreshProfiles(); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + + handleMakeAdmin(e) { + e.preventDefault(); + const data = { + user_id: this.props.user.id, + new_roles: 'admin' + }; + + Client.updateRoles(data, + () => { + this.props.refreshProfiles(); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + + handleMakeSystemAdmin(e) { + e.preventDefault(); + const data = { + user_id: this.props.user.id, + new_roles: 'system_admin' + }; + + Client.updateRoles(data, + () => { + this.props.refreshProfiles(); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + + handleResetPassword(e) { + e.preventDefault(); + this.props.doPasswordReset(this.props.user); + } + + render() { + let serverError = null; + if (this.state.serverError) { + serverError = ( + <div className='has-error'> + <label className='has-error control-label'>{this.state.serverError}</label> + </div> + ); + } + + const user = this.props.user; + let currentRoles = 'Member'; + if (user.roles.length > 0) { + if (user.roles.indexOf('system_admin') > -1) { + currentRoles = 'System Admin'; + } else { + currentRoles = user.roles.charAt(0).toUpperCase() + user.roles.slice(1); + } + } + + const email = user.email; + let showMakeMember = user.roles === 'admin' || user.roles === 'system_admin'; + let showMakeAdmin = user.roles === '' || user.roles === 'system_admin'; + let showMakeSystemAdmin = user.roles === '' || user.roles === 'admin'; + let showMakeActive = false; + let showMakeNotActive = user.roles !== 'system_admin'; + + if (user.delete_at > 0) { + currentRoles = 'Inactive'; + currentRoles = 'Inactive'; + showMakeMember = false; + showMakeAdmin = false; + showMakeSystemAdmin = false; + showMakeActive = true; + showMakeNotActive = false; + } + + let makeSystemAdmin = null; + if (showMakeSystemAdmin) { + makeSystemAdmin = ( + <li role='presentation'> + <a + role='menuitem' + href='#' + onClick={this.handleMakeSystemAdmin} + > + {'Make System Admin'} + </a> + </li> + ); + } + + let makeAdmin = null; + if (showMakeAdmin) { + makeAdmin = ( + <li role='presentation'> + <a + role='menuitem' + href='#' + onClick={this.handleMakeAdmin} + > + {'Make Admin'} + </a> + </li> + ); + } + + let makeMember = null; + if (showMakeMember) { + makeMember = ( + <li role='presentation'> + <a + role='menuitem' + href='#' + onClick={this.handleMakeMember} + > + {'Make Member'} + </a> + </li> + ); + } + + let makeActive = null; + if (showMakeActive) { + makeActive = ( + <li role='presentation'> + <a + role='menuitem' + href='#' + onClick={this.handleMakeActive} + > + {'Make Active'} + </a> + </li> + ); + } + + let makeNotActive = null; + if (showMakeNotActive) { + makeNotActive = ( + <li role='presentation'> + <a + role='menuitem' + href='#' + onClick={this.handleMakeNotActive} + > + {'Make Inactive'} + </a> + </li> + ); + } + + return ( + <div className='row member-div'> + <img + className='post-profile-img pull-left' + src={`/api/v1/users/${user.id}/image?time=${user.update_at}`} + height='36' + width='36' + /> + <span className='member-name'>{Utils.getDisplayName(user)}</span> + <span className='member-email'>{email}</span> + <div className='dropdown member-drop'> + <a + href='#' + className='dropdown-toggle theme' + type='button' + id='channel_header_dropdown' + data-toggle='dropdown' + aria-expanded='true' + > + <span>{currentRoles} </span> + <span className='caret'></span> + </a> + <ul + className='dropdown-menu member-menu' + role='menu' + aria-labelledby='channel_header_dropdown' + > + {makeAdmin} + {makeMember} + {makeActive} + {makeNotActive} + {makeSystemAdmin} + <li role='presentation'> + <a + role='menuitem' + href='#' + onClick={this.handleResetPassword} + > + {'Reset Password'} + </a> + </li> + </ul> + </div> + {serverError} + </div> + ); + } +} + +UserItem.propTypes = { + user: React.PropTypes.object.isRequired, + refreshProfiles: React.PropTypes.func.isRequired, + doPasswordReset: React.PropTypes.func.isRequired +}; diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx index 962ba26ee..39c86405c 100644 --- a/web/react/components/channel_loader.jsx +++ b/web/react/components/channel_loader.jsx @@ -109,6 +109,13 @@ export default class ChannelLoader extends React.Component { $('.modal-body').css('overflow-y', 'auto'); $('.modal-body').css('max-height', $(window).height() * 0.7); }); + + /* Prevent backspace from navigating back a page */ + $(window).on('keydown.preventBackspace', (e) => { + if (e.which === 8 && !$(e.target).is('input, textarea')) { + e.preventDefault(); + } + }); } componentWillUnmount() { clearInterval(this.intervalId); @@ -123,6 +130,8 @@ export default class ChannelLoader extends React.Component { $('body').off('mouseenter mouseleave', '.post.post--comment.same--root'); $('.modal').off('show.bs.modal'); + + $(window).off('keydown.preventBackspace'); } onSocketChange(msg) { if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) { diff --git a/web/react/components/channel_notifications.jsx b/web/react/components/channel_notifications.jsx index 83067240d..9eda68b38 100644 --- a/web/react/components/channel_notifications.jsx +++ b/web/react/components/channel_notifications.jsx @@ -163,10 +163,22 @@ export default class ChannelNotifications extends React.Component { }.bind(this); let curChannel = ChannelStore.get(this.state.channelId); - let extraInfo = (<span>These settings will override the global notification settings</span>); + let extraInfo = ( + <span> + These settings will override the global notification settings. + <br/> + Desktop notifications are available on Firefox, Safari, and Chrome. + </span> + ); if (curChannel && curChannel.display_name) { - extraInfo = (<span>These settings will override the global notification settings for the <b>{curChannel.display_name}</b> channel</span>); + extraInfo = ( + <span> + These settings will override the global notification settings for the <b>{curChannel.display_name}</b> channel. + <br/> + Desktop notifications are available on Firefox, Safari, and Chrome. + </span> + ); } return ( diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx index c2fc0dcf3..99f553c0c 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -28,6 +28,7 @@ export default class CreateComment extends React.Component { this.handleUploadStart = this.handleUploadStart.bind(this); this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this); this.handleUploadError = this.handleUploadError.bind(this); + this.handleTextDrop = this.handleTextDrop.bind(this); this.removePreview = this.removePreview.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.getFileCount = this.getFileCount.bind(this); @@ -178,6 +179,11 @@ export default class CreateComment extends React.Component { this.setState({serverError: err}); } } + handleTextDrop(text) { + const newText = this.state.messageText + text; + this.handleUserInput(newText); + Utils.setCaretPosition(React.findDOMNode(this.refs.textbox.refs.message), newText.length); + } removePreview(id) { let previews = this.state.previews; let uploadsInProgress = this.state.uploadsInProgress; @@ -264,6 +270,7 @@ export default class CreateComment extends React.Component { onUploadStart={this.handleUploadStart} onFileUpload={this.handleFileUploadComplete} onUploadError={this.handleUploadError} + onTextDrop={this.handleTextDrop} postType='comment' channelId={this.props.channelId} /> diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index abad60154..595643027 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -31,6 +31,7 @@ export default class CreatePost extends React.Component { this.handleUploadStart = this.handleUploadStart.bind(this); this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this); this.handleUploadError = this.handleUploadError.bind(this); + this.handleTextDrop = this.handleTextDrop.bind(this); this.removePreview = this.removePreview.bind(this); this.onChange = this.onChange.bind(this); this.getFileCount = this.getFileCount.bind(this); @@ -230,6 +231,11 @@ export default class CreatePost extends React.Component { this.setState({serverError: err}); } } + handleTextDrop(text) { + const newText = this.state.messageText + text; + this.handleUserInput(newText); + Utils.setCaretPosition(React.findDOMNode(this.refs.textbox.refs.message), newText.length); + } removePreview(id) { let previews = this.state.previews; let uploadsInProgress = this.state.uploadsInProgress; @@ -334,6 +340,7 @@ export default class CreatePost extends React.Component { onUploadStart={this.handleUploadStart} onFileUpload={this.handleFileUploadComplete} onUploadError={this.handleUploadError} + onTextDrop={this.handleTextDrop} postType='post' channelId='' /> diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx index c9aa06a97..888f24aa5 100644 --- a/web/react/components/file_attachment.jsx +++ b/web/react/components/file_attachment.jsx @@ -143,10 +143,7 @@ export default class FileAttachment extends React.Component { > <a className='post-image__thumbnail' href='#' - onClick={this.props.handleImageClick} - data-img-id={this.props.index} - data-toggle='modal' - data-target={'#' + this.props.modalId} + onClick={() => this.props.handleImageClick(this.props.index)} > {thumbnail} </a> @@ -187,9 +184,6 @@ FileAttachment.propTypes = { // the index of this attachment preview in the parent FileAttachmentList index: React.PropTypes.number.isRequired, - // the identifier of the modal dialog used to preview files - modalId: React.PropTypes.string.isRequired, - - // handler for when the thumbnail is clicked + // handler for when the thumbnail is clicked passed the index above handleImageClick: React.PropTypes.func }; diff --git a/web/react/components/file_attachment_list.jsx b/web/react/components/file_attachment_list.jsx index abe72089a..212d4a958 100644 --- a/web/react/components/file_attachment_list.jsx +++ b/web/react/components/file_attachment_list.jsx @@ -11,23 +11,21 @@ export default class FileAttachmentList extends React.Component { this.handleImageClick = this.handleImageClick.bind(this); - this.state = {startImgId: 0}; + this.state = {showPreviewModal: false, startImgId: 0}; } - handleImageClick(e) { - this.setState({startImgId: parseInt($(e.target.parentNode).attr('data-img-id'), 10)}); + handleImageClick(indexClicked) { + this.setState({showPreviewModal: true, startImgId: indexClicked}); } render() { var filenames = this.props.filenames; - var modalId = this.props.modalId; var postFiles = []; for (var i = 0; i < filenames.length && i < Constants.MAX_DISPLAY_FILES; i++) { postFiles.push( <FileAttachment - key={i} + key={'file_attachment_' + i} filename={filenames[i]} index={i} - modalId={modalId} handleImageClick={this.handleImageClick} /> ); @@ -39,9 +37,10 @@ export default class FileAttachmentList extends React.Component { {postFiles} </div> <ViewImageModal + show={this.state.showPreviewModal} + onModalDismissed={() => this.setState({showPreviewModal: false})} channelId={this.props.channelId} userId={this.props.userId} - modalId={modalId} startId={this.state.startImgId} filenames={filenames} /> @@ -55,9 +54,6 @@ FileAttachmentList.propTypes = { // a list of file pathes displayed by this filenames: React.PropTypes.arrayOf(React.PropTypes.string).isRequired, - // the identifier of the modal dialog used to preview files - modalId: React.PropTypes.string.isRequired, - // the channel that this is part of channelId: React.PropTypes.string, diff --git a/web/react/components/file_upload.jsx b/web/react/components/file_upload.jsx index 3cb284171..3dc4e5de2 100644 --- a/web/react/components/file_upload.jsx +++ b/web/react/components/file_upload.jsx @@ -110,7 +110,7 @@ export default class FileUpload extends React.Component { if (typeof files !== 'string' && files.length) { this.uploadFiles(files); } else { - this.props.onUploadError('Invalid file upload', -1); + this.props.onTextDrop(e.originalEvent.dataTransfer.getData('Text')); } } @@ -266,6 +266,7 @@ FileUpload.propTypes = { getFileCount: React.PropTypes.func, onFileUpload: React.PropTypes.func, onUploadStart: React.PropTypes.func, + onTextDrop: React.PropTypes.func, channelId: React.PropTypes.string, postType: React.PropTypes.string }; diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index dbbcdc409..6e98e4aba 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -141,7 +141,6 @@ export default class PostBody extends React.Component { fileAttachmentHolder = ( <FileAttachmentList filenames={filenames} - modalId={`view_image_modal_${post.id}`} channelId={post.channel_id} userId={post.user_id} /> diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx index d2a0a4035..c38edf6a2 100644 --- a/web/react/components/post_info.jsx +++ b/web/react/components/post_info.jsx @@ -153,7 +153,7 @@ export default class PostInfo extends React.Component { <li className='post-header-col'> <time className='post-profile-time' - title={new Date(post.create_at).toString()} + title={`${utils.displayDate(post.create_at)} at ${utils.displayTime(post.create_at)}`} > {utils.displayDateTime(post.create_at)} </time> diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx index 4d1892a69..5b4694eb1 100644 --- a/web/react/components/rhs_comment.jsx +++ b/web/react/components/rhs_comment.jsx @@ -163,7 +163,6 @@ export default class RhsComment extends React.Component { fileAttachment = ( <FileAttachmentList filenames={post.filenames} - modalId={'rhs_comment_view_image_modal_' + post.id} channelId={post.channel_id} userId={post.user_id} /> @@ -186,10 +185,7 @@ export default class RhsComment extends React.Component { <strong><UserProfile userId={post.user_id} /></strong> </li> <li className='post-header-col'> - <time - className='post-profile-time' - title={new Date(post.create_at).toString()} - > + <time className='post-profile-time'> {Utils.displayCommentDateTime(post.create_at)} </time> </li> diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx index e661bdce1..13ab0c982 100644 --- a/web/react/components/rhs_root_post.jsx +++ b/web/react/components/rhs_root_post.jsx @@ -111,7 +111,6 @@ export default class RhsRootPost extends React.Component { fileAttachment = ( <FileAttachmentList filenames={post.filenames} - modalId={'rhs_view_image_modal_' + post.id} channelId={post.channel_id} userId={post.user_id} /> @@ -133,10 +132,7 @@ export default class RhsRootPost extends React.Component { <ul className='post-header'> <li className='post-header-col'><strong><UserProfile userId={post.user_id} /></strong></li> <li className='post-header-col'> - <time - className='post-profile-time' - title={new Date(post.create_at).toString()} - > + <time className='post-profile-time'> {utils.displayCommentDateTime(post.create_at)} </time> </li> diff --git a/web/react/components/settings_sidebar.jsx b/web/react/components/settings_sidebar.jsx index e5cbd6e92..4c4675788 100644 --- a/web/react/components/settings_sidebar.jsx +++ b/web/react/components/settings_sidebar.jsx @@ -7,7 +7,8 @@ export default class SettingsSidebar extends React.Component { this.handleClick = this.handleClick.bind(this); } - handleClick(tab) { + handleClick(tab, e) { + e.preventDefault(); this.props.updateTab(tab.name); $('.settings-modal').addClass('display--content'); } diff --git a/web/react/components/team_import_tab.jsx b/web/react/components/team_import_tab.jsx index 8315430e4..79f03510f 100644 --- a/web/react/components/team_import_tab.jsx +++ b/web/react/components/team_import_tab.jsx @@ -35,7 +35,7 @@ export default class TeamImportTab extends React.Component { var uploadHelpText = ( <div> <p>{'Slack does not allow you to export files, images, private groups or direct messages stored in Slack. Therefore, Slack import to Mattermost only supports importing of text messages in your Slack team\'\s public channels.'}</p> - <p>{'The Slack import to Mattermost is in "Preview". Slack bot posts do not yet import.'}</p> + <p>{'The Slack import to Mattermost is in "Preview". Slack bot posts do not yet import and Slack @mentions are not currently supported.'}</p> </div> ); diff --git a/web/react/components/team_settings_modal.jsx b/web/react/components/team_settings_modal.jsx index 0513c811f..8ffbdc6d0 100644 --- a/web/react/components/team_settings_modal.jsx +++ b/web/react/components/team_settings_modal.jsx @@ -36,7 +36,9 @@ export default class TeamSettingsModal extends React.Component { let tabs = []; tabs.push({name: 'general', uiName: 'General', icon: 'glyphicon glyphicon-cog'}); tabs.push({name: 'import', uiName: 'Import', icon: 'glyphicon glyphicon-upload'}); - tabs.push({name: 'export', uiName: 'Export', icon: 'glyphicon glyphicon-download'}); + + // To enable export uncomment this line + //tabs.push({name: 'export', uiName: 'Export', icon: 'glyphicon glyphicon-download'}); tabs.push({name: 'feature', uiName: 'Advanced', icon: 'glyphicon glyphicon-wrench'}); return ( diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx index 44630a318..c680d75d1 100644 --- a/web/react/components/user_settings/custom_theme_chooser.jsx +++ b/web/react/components/user_settings/custom_theme_chooser.jsx @@ -53,9 +53,12 @@ export default class CustomThemeChooser extends React.Component { const elements = []; let colors = ''; - Constants.THEME_ELEMENTS.forEach((element) => { + Constants.THEME_ELEMENTS.forEach((element, index) => { elements.push( - <div className='col-sm-4 form-group'> + <div + className='col-sm-4 form-group' + key={'custom-theme-key' + index} + > <label className='custom-label'>{element.uiName}</label> <div className='input-group color-picker' diff --git a/web/react/components/user_settings/manage_incoming_hooks.jsx b/web/react/components/user_settings/manage_incoming_hooks.jsx index df089a403..12c041c7f 100644 --- a/web/react/components/user_settings/manage_incoming_hooks.jsx +++ b/web/react/components/user_settings/manage_incoming_hooks.jsx @@ -107,23 +107,23 @@ export default class ManageIncomingHooks extends React.Component { this.state.hooks.forEach((hook) => { const c = ChannelStore.get(hook.channel_id); hooks.push( - <div> - <div className='divider-light'></div> - <span> - <strong>{'URL: '}</strong>{Utils.getWindowLocationOrigin() + '/hooks/' + hook.id} - </span> - <br/> - <span> + <div className='font--small'> + <div className='padding-top x2 divider-light'></div> + <div className='padding-top x2'> + <strong>{'URL: '}</strong><span className='word-break--all'>{Utils.getWindowLocationOrigin() + '/hooks/' + hook.id}</span> + </div> + <div className='padding-top'> <strong>{'Channel: '}</strong>{c.name} - </span> - <br/> - <a - className={'btn btn-sm btn-primary'} - href='#' - onClick={this.removeHook.bind(this, hook.id)} - > - {'Remove'} - </a> + </div> + <div className='padding-top'> + <a + className={'text-danger'} + href='#' + onClick={this.removeHook.bind(this, hook.id)} + > + {'Remove'} + </a> + </div> </div> ); }); @@ -134,41 +134,38 @@ export default class ManageIncomingHooks extends React.Component { } else if (hooks.length > 0) { displayHooks = hooks; } else { - displayHooks = <label>{'None'}</label>; + displayHooks = <label>{' None'}</label>; } const existingHooks = ( - <div> - <label className='control-label'>{'Existing incoming webhooks'}</label> - <br/> + <div className='padding-top x2'> + <label className='control-label padding-top x2'>{'Existing incoming webhooks'}</label> {displayHooks} </div> ); return ( - <div - key='addIncomingHook' - className='form-group' - > + <div key='addIncomingHook'> <label className='control-label'>{'Add a new incoming webhook'}</label> - <br/> - <div> + <div className='padding-top'> <select ref='channelName' + className='form-control' value={this.state.channelId} onChange={this.updateChannelId} > {options} </select> - <br/> {serverError} - <a - className={'btn btn-sm btn-primary' + disableButton} - href='#' - onClick={this.addNewHook} - > - {'Add'} - </a> + <div className='padding-top'> + <a + className={'btn btn-sm btn-primary' + disableButton} + href='#' + onClick={this.addNewHook} + > + {'Add'} + </a> + </div> </div> {existingHooks} </div> diff --git a/web/react/components/user_settings/premade_theme_chooser.jsx b/web/react/components/user_settings/premade_theme_chooser.jsx index e6aa2f5b9..f8f916bd0 100644 --- a/web/react/components/user_settings/premade_theme_chooser.jsx +++ b/web/react/components/user_settings/premade_theme_chooser.jsx @@ -23,7 +23,10 @@ export default class PremadeThemeChooser extends React.Component { } premadeThemes.push( - <div className='col-sm-3 premade-themes'> + <div + className='col-sm-3 premade-themes' + key={'premade-theme-key' + k} + > <div className={activeClass} onClick={() => this.props.updateTheme(premadeTheme)} diff --git a/web/react/components/user_settings/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx index 8d364cde7..ba14f019f 100644 --- a/web/react/components/user_settings/user_settings_notifications.jsx +++ b/web/react/components/user_settings/user_settings_notifications.jsx @@ -265,9 +265,12 @@ export default class NotificationsTab extends React.Component { e.preventDefault(); }.bind(this); + const extraInfo = <span>{'Desktop notifications are available on Firefox, Safari, and Chrome.'}</span>; + desktopSection = ( <SettingItemMax title='Send desktop notifications' + extraInfo={extraInfo} inputs={inputs} submit={this.handleSubmit} server_error={serverError} @@ -343,9 +346,12 @@ export default class NotificationsTab extends React.Component { e.preventDefault(); }.bind(this); + const extraInfo = <span>{'Desktop notification sounds are available on Firefox, Safari, Chrome, Internet Explorer, and Edge.'}</span>; + soundSection = ( <SettingItemMax title='Desktop notification sounds' + extraInfo={extraInfo} inputs={inputs} submit={this.handleSubmit} server_error={serverError} diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx index dafcdd9f9..e645878c1 100644 --- a/web/react/components/view_image.jsx +++ b/web/react/components/view_image.jsx @@ -3,6 +3,8 @@ var Client = require('../utils/client.jsx'); var Utils = require('../utils/utils.jsx'); +var ViewImagePopoverBar = require('./view_image_popover_bar.jsx'); +var Modal = ReactBootstrap.Modal; export default class ViewImageModal extends React.Component { constructor(props) { @@ -16,6 +18,10 @@ export default class ViewImageModal extends React.Component { this.handleKeyPress = this.handleKeyPress.bind(this); this.getPublicLink = this.getPublicLink.bind(this); this.getPreviewImagePath = this.getPreviewImagePath.bind(this); + this.onModalShown = this.onModalShown.bind(this); + this.onModalHidden = this.onModalHidden.bind(this); + this.onMouseEnterImage = this.onMouseEnterImage.bind(this); + this.onMouseLeaveImage = this.onMouseLeaveImage.bind(this); var loaded = []; var progress = []; @@ -23,9 +29,20 @@ export default class ViewImageModal extends React.Component { loaded.push(false); progress.push(0); } - this.state = {imgId: this.props.startId, viewed: false, loaded: loaded, progress: progress, images: {}, fileSizes: {}}; + this.state = { + imgId: this.props.startId, + imgHeight: '100%', + loaded: loaded, + progress: progress, + images: {}, + fileSizes: {}, + showFooter: false + }; } - handleNext() { + handleNext(e) { + if (e) { + e.stopPropagation(); + } var id = this.state.imgId + 1; if (id > this.props.filenames.length - 1) { id = 0; @@ -33,7 +50,10 @@ export default class ViewImageModal extends React.Component { this.setState({imgId: id}); this.loadImage(id); } - handlePrev() { + handlePrev(e) { + if (e) { + e.stopPropagation(); + } var id = this.state.imgId - 1; if (id < 0) { id = this.props.filenames.length - 1; @@ -50,15 +70,27 @@ export default class ViewImageModal extends React.Component { this.handlePrev(); } } - componentWillReceiveProps(nextProps) { + onModalShown(nextProps) { this.setState({imgId: nextProps.startId}); + this.loadImage(nextProps.startId); + } + onModalHidden() { + if (this.refs.video) { + var video = React.findDOMNode(this.refs.video); + video.pause(); + video.currentTime = 0; + } + } + componentWillReceiveProps(nextProps) { + if (nextProps.show === true && this.props.show === false) { + this.onModalShown(nextProps); + } else if (nextProps.show === false && this.props.show === true) { + this.onModalHidden(); + } } 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); - return; - } + this.setState({imgHeight}); var filename = this.props.filenames[id]; @@ -68,84 +100,27 @@ export default class ViewImageModal extends React.Component { if (fileType === 'image') { var img = new Image(); img.load(this.getPreviewImagePath(filename), - function load() { - var progress = this.state.progress; - progress[id] = img.completedPercentage; - this.setState({progress: progress}); - }.bind(this)); - img.onload = (function onload(imgid) { - return function onloadReturn() { - var loaded = this.state.loaded; - loaded[imgid] = true; - this.setState({loaded: loaded}); - $(React.findDOMNode(this.refs.image)).css('max-height', imgHeight); - }.bind(this); - }.bind(this)(id)); + () => { + const progress = this.state.progress; + progress[id] = img.completedPercentage; + this.setState({progress}); + }); + img.onload = () => { + const loaded = this.state.loaded; + loaded[id] = true; + this.setState({loaded}); + }; var images = this.state.images; images[id] = img; - this.setState({images: images}); + this.setState({images}); } else { // there's nothing to load for non-image files var loaded = this.state.loaded; loaded[id] = true; - this.setState({loaded: loaded}); - } - } - componentDidUpdate() { - if (this.state.loaded[this.state.imgId]) { - if (this.refs.imageWrap) { - $(React.findDOMNode(this.refs.imageWrap)).removeClass('default'); - } + this.setState({loaded}); } } componentDidMount() { - $('#' + this.props.modalId).on('shown.bs.modal', function onModalShow() { - this.setState({viewed: true}); - this.loadImage(this.state.imgId); - }.bind(this)); - - $('#' + this.props.modalId).on('hidden.bs.modal', function onModalHide() { - if (this.refs.video) { - var video = React.findDOMNode(this.refs.video); - video.pause(); - video.currentTime = 0; - } - }.bind(this)); - - $(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)); - - $(React.findDOMNode(this.refs.imageWrap)).hover( - function onModalHover() { - $(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) { - $(React.findDOMNode(this.refs.previewArrowLeft)).hover( - function onModalHover() { - $(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) { - $(React.findDOMNode(this.refs.previewArrowRight)).hover( - function onModalHover() { - $(React.findDOMNode(this.refs.imageFooter)).addClass('footer--show'); - }.bind(this), function offModalHover() { - $(React.findDOMNode(this.refs.imageFooter)).removeClass('footer--show'); - }.bind(this) - ); - } - $(window).on('keyup', this.handleKeyPress); // keep track of whether or not this component is mounted so we can safely set the state asynchronously @@ -189,6 +164,12 @@ export default class ViewImageModal extends React.Component { // only images have proper previews, so just use a placeholder icon for non-images return Utils.getPreviewImagePathForFileType(fileType); } + onMouseEnterImage() { + this.setState({showFooter: true}); + } + onMouseLeaveImage() { + this.setState({showFooter: false}); + } render() { if (this.props.filenames.length < 1 || this.props.filenames.length - 1 < this.state.imgId) { return <div/>; @@ -299,23 +280,6 @@ export default class ViewImageModal extends React.Component { bgClass = 'black-bg'; } - var publicLink = ''; - if (global.window.config.EnablePublicLink === 'true') { - publicLink = ( - <div> - <a - href='#' - className='public-link text' - data-title='Public Image' - onClick={this.getPublicLink} - > - Get Public Link - </a> - <span className='text'> | </span> - </div> - ); - } - var leftArrow = ''; var rightArrow = ''; if (this.props.filenames.length > 1) { @@ -342,65 +306,61 @@ export default class ViewImageModal extends React.Component { ); } + let closeButtonClass = 'modal-close'; + if (this.state.showFooter) { + closeButtonClass += ' modal-close--show'; + } + return ( - <div - className='modal fade image_modal' - ref='modal' - id={this.props.modalId} - tabIndex='-1' - role='dialog' - aria-hidden='true' + <Modal + show={this.props.show} + onHide={this.props.onModalDismissed} + className='image_modal' + dialogClassName='modal-image' > - <div className='modal-dialog modal-image'> - <div className='modal-content image-content'> + <Modal.Body + modalClassName='image-body' + onClick={this.props.onModalDismissed} + > + <div + className={'image-wrapper ' + bgClass} + style={{maxHeight: this.state.imgHeight}} + onMouseEnter={this.onMouseEnterImage} + onMouseLeave={this.onMouseLeaveImage} + onClick={(e) => e.stopPropagation()} + > <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' - > - <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> - </div> - </div> - </div> - {leftArrow} - {rightArrow} - </div> + className={closeButtonClass} + onClick={this.props.onModalDismissed} + /> + {content} + <ViewImagePopoverBar + show={this.state.showFooter} + fileId={this.state.imgId} + totalFiles={this.props.filenames.length} + filename={name} + fileURL={fileUrl} + onGetPublicLinkPressed={this.getPublicLink} + /> </div> - </div> - </div> + {leftArrow} + {rightArrow} + </Modal.Body> + </Modal> ); } } ViewImageModal.defaultProps = { + show: false, filenames: [], - modalId: '', channelId: '', userId: '', startId: 0 }; ViewImageModal.propTypes = { + show: React.PropTypes.bool.isRequired, + onModalDismissed: React.PropTypes.func.isRequired, filenames: React.PropTypes.array, modalId: React.PropTypes.string, channelId: React.PropTypes.string, diff --git a/web/react/components/view_image_popover_bar.jsx b/web/react/components/view_image_popover_bar.jsx new file mode 100644 index 000000000..68817d751 --- /dev/null +++ b/web/react/components/view_image_popover_bar.jsx @@ -0,0 +1,66 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +export default class ViewImagePopoverBar extends React.Component { + constructor(props) { + super(props); + } + render() { + var publicLink = ''; + if (global.window.config.EnablePublicLink === 'true') { + publicLink = ( + <div> + <a + href='#' + className='public-link text' + data-title='Public Image' + onClick={this.getPublicLink} + > + {'Get Public Link'} + </a> + <span className='text'>{' | '}</span> + </div> + ); + } + + var footerClass = 'modal-button-bar'; + if (this.props.show) { + footerClass += ' footer--show'; + } + + return ( + <div + ref='imageFooter' + className={footerClass} + > + <span className='pull-left text'>{'File ' + (this.props.fileId + 1) + ' of ' + this.props.totalFiles}</span> + <div className='image-links'> + {publicLink} + <a + href={this.props.fileURL} + download={this.props.filename} + className='text' + > + {'Download'} + </a> + </div> + </div> + ); + } +} +ViewImagePopoverBar.defaultProps = { + show: false, + imgId: 0, + totalFiles: 0, + filename: '', + fileURL: '' +}; + +ViewImagePopoverBar.propTypes = { + show: React.PropTypes.bool.isRequired, + fileId: React.PropTypes.number.isRequired, + totalFiles: React.PropTypes.number.isRequired, + filename: React.PropTypes.string.isRequired, + fileURL: React.PropTypes.string.isRequired, + onGetPublicLinkPressed: React.PropTypes.func.isRequired +}; diff --git a/web/react/stores/admin_store.jsx b/web/react/stores/admin_store.jsx index dd5b60a24..7b2aeb631 100644 --- a/web/react/stores/admin_store.jsx +++ b/web/react/stores/admin_store.jsx @@ -4,11 +4,14 @@ var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var EventEmitter = require('events').EventEmitter; +var BrowserStore = require('../stores/browser_store.jsx'); + var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; var LOG_CHANGE_EVENT = 'log_change'; var CONFIG_CHANGE_EVENT = 'config_change'; +var ALL_TEAMS_EVENT = 'all_team_change'; class AdminStoreClass extends EventEmitter { constructor() { @@ -16,6 +19,7 @@ class AdminStoreClass extends EventEmitter { this.logs = null; this.config = null; + this.teams = null; this.emitLogChange = this.emitLogChange.bind(this); this.addLogChangeListener = this.addLogChangeListener.bind(this); @@ -24,6 +28,10 @@ class AdminStoreClass extends EventEmitter { this.emitConfigChange = this.emitConfigChange.bind(this); this.addConfigChangeListener = this.addConfigChangeListener.bind(this); this.removeConfigChangeListener = this.removeConfigChangeListener.bind(this); + + this.emitAllTeamsChange = this.emitAllTeamsChange.bind(this); + this.addAllTeamsChangeListener = this.addAllTeamsChangeListener.bind(this); + this.removeAllTeamsChangeListener = this.removeAllTeamsChangeListener.bind(this); } emitLogChange() { @@ -50,6 +58,18 @@ class AdminStoreClass extends EventEmitter { this.removeListener(CONFIG_CHANGE_EVENT, callback); } + emitAllTeamsChange() { + this.emit(ALL_TEAMS_EVENT); + } + + addAllTeamsChangeListener(callback) { + this.on(ALL_TEAMS_EVENT, callback); + } + + removeAllTeamsChangeListener(callback) { + this.removeListener(ALL_TEAMS_EVENT, callback); + } + getLogs() { return this.logs; } @@ -65,6 +85,22 @@ class AdminStoreClass extends EventEmitter { saveConfig(config) { this.config = config; } + + getAllTeams() { + return this.teams; + } + + saveAllTeams(teams) { + this.teams = teams; + } + + getSelectedTeams() { + return BrowserStore.getItem('seleted_teams'); + } + + saveSelectedTeams(teams) { + BrowserStore.setItem('seleted_teams', teams); + } } var AdminStore = new AdminStoreClass(); @@ -81,6 +117,10 @@ AdminStoreClass.dispatchToken = AppDispatcher.register((payload) => { AdminStore.saveConfig(action.config); AdminStore.emitConfigChange(); break; + case ActionTypes.RECIEVED_ALL_TEAMS: + AdminStore.saveAllTeams(action.teams); + AdminStore.emitAllTeamsChange(); + break; default: } }); diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index ed228f6c4..ab2965000 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -371,6 +371,32 @@ export function getConfig() { ); } +export function getAllTeams() { + if (isCallInProgress('getAllTeams')) { + return; + } + + callTracker.getAllTeams = utils.getTimestamp(); + client.getAllTeams( + (data, textStatus, xhr) => { + callTracker.getAllTeams = 0; + + if (xhr.status === 304 || !data) { + return; + } + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_ALL_TEAMS, + teams: data + }); + }, + (err) => { + callTracker.getAllTeams = 0; + dispatchError(err, 'getAllTeams'); + } + ); +} + export function findTeams(email) { if (isCallInProgress('findTeams_' + email)) { return; diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index a19f58e61..63924bff2 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -347,6 +347,20 @@ export function testEmail(config, success, error) { }); } +export function getAllTeams(success, error) { + $.ajax({ + url: '/api/v1/teams/all', + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: function onError(xhr, status, err) { + var e = handleError('getAllTeams', xhr, status, err); + error(e); + } + }); +} + export function getMeSynchronous(success, error) { var currentUser = null; $.ajax({ @@ -890,6 +904,21 @@ export function getProfiles(success, error) { }); } +export function getProfilesForTeam(teamId, success, error) { + $.ajax({ + cache: false, + url: '/api/v1/users/profiles/' + teamId, + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: function onError(xhr, status, err) { + var e = handleError('getProfilesForTeam', xhr, status, err); + error(e); + } + }); +} + export function uploadFile(formData, success, error) { var request = $.ajax({ url: '/api/v1/files/upload', diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 41e9e9ca6..90af9beda 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -35,8 +35,8 @@ module.exports = { RECIEVED_TEAM: null, RECIEVED_CONFIG: null, - RECIEVED_LOGS: null, + RECIEVED_ALL_TEAMS: null, TOGGLE_IMPORT_THEME_MODAL: null }), @@ -134,24 +134,24 @@ module.exports = { buttonBg: '#2389d7', buttonColor: '#FFFFFF' }, - slack: { - type: 'Slack', - sidebarBg: '#4D394B', - sidebarText: '#ab9ba9', - sidebarUnreadText: '#FFFFFF', - sidebarTextHoverBg: '#3e313c', - sidebarTextHoverColor: '#ab9ba9', - sidebarTextActiveBg: '#4c9689', + organization: { + type: 'Organization', + sidebarBg: '#2071a7', + sidebarText: '#bfcde8', + sidebarUnreadText: '#fff', + sidebarTextHoverBg: '#136197', + sidebarTextHoverColor: '#bfcde8', + sidebarTextActiveBg: '#136197', sidebarTextActiveColor: '#FFFFFF', - sidebarHeaderBg: '#4D394B', + sidebarHeaderBg: '#2f81b7', sidebarHeaderTextColor: '#FFFFFF', - onlineIndicator: '#4c9689', - mentionBj: '#eb4d5c', - mentionColor: '#FFFFFF', - centerChannelBg: '#FFFFFF', + onlineIndicator: '#7DBE00', + mentionBj: '#136197', + mentionColor: '#bfcde8', + centerChannelBg: '#f2f4f8', centerChannelColor: '#333333', - linkColor: '#2389d7', - buttonBg: '#26a970', + linkColor: '#2f81b7', + buttonBg: '#1dacfc', buttonColor: '#FFFFFF' }, dark: { @@ -165,13 +165,13 @@ module.exports = { sidebarTextActiveColor: '#FFFFFF', sidebarHeaderBg: '#1B2C3E', sidebarHeaderTextColor: '#FFFFFF', - onlineIndicator: '#4c9689', + onlineIndicator: '#55C5B2', mentionBj: '#B74A4A', mentionColor: '#FFFFFF', centerChannelBg: '#2F3E4E', centerChannelColor: '#DDDDDD', linkColor: '#A4FFEB', - buttonBg: '#2B9C99', + buttonBg: '#1dacfc', buttonColor: '#FFFFFF' } }, diff --git a/web/react/utils/emoticons.jsx b/web/react/utils/emoticons.jsx index 7210201ff..a7c837199 100644 --- a/web/react/utils/emoticons.jsx +++ b/web/react/utils/emoticons.jsx @@ -5,15 +5,14 @@ const emoticonPatterns = { smile: /:-?\)/g, // :) open_mouth: /:o/gi, // :o scream: /:-o/gi, // :-o - smirk: /[:;]-?]/g, // :] - grinning: /[:;]-?d/gi, // :D + smirk: /:-?]/g, // :] + grinning: /:-?d/gi, // :D stuck_out_tongue_closed_eyes: /x-d/gi, // x-d - stuck_out_tongue_winking_eye: /[:;]-?p/gi, // ;p + stuck_out_tongue_winking_eye: /:-?p/gi, // :p rage: /:-?[\[@]/g, // :@ frowning: /:-?\(/g, // :( sob: /:['’]-?\(|:'\(/g, // :`( kissing_heart: /:-?\*/g, // :* - wink: /;-?\)/g, // ;) pensive: /:-?\//g, // :/ confounded: /:-?s/gi, // :s flushed: /:-?\|/g, // :| diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx index c6ffb1871..7e88f8644 100644 --- a/web/react/utils/markdown.jsx +++ b/web/react/utils/markdown.jsx @@ -15,6 +15,14 @@ export class MattermostMarkdownRenderer extends marked.Renderer { this.formattingOptions = formattingOptions; } + br() { + if (this.formattingOptions.singleline) { + return ' '; + } + + return super.br(); + } + heading(text, level, raw) { const id = `${this.options.headerPrefix}${raw.toLowerCase().replace(/[^\w]+/g, '-')}`; return `<h${level} id="${id}" class="markdown__heading">${text}</h${level}>`; @@ -36,6 +44,14 @@ export class MattermostMarkdownRenderer extends marked.Renderer { return output; } + paragraph(text) { + if (this.formattingOptions.singleline) { + return `<p class="markdown__paragraph-inline">${text}</p>`; + } + + return super.paragraph(text); + } + table(header, body) { return `<table class="markdown__table"><thead>${header}</thead><tbody>${body}</tbody></table>`; } diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 82bb82d6b..50438c6cf 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -553,7 +553,6 @@ export function applyTheme(theme) { changeCss('.sidebar--left .nav li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.8), 1); changeCss('.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + theme.sidebarText, 1); changeCss('.sidebar--left, .sidebar--right .sidebar--right__header', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 1); - changeCss('.sidebar--right .sidebar-right__body', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 2); changeCss('.sidebar--left .status path', 'fill:' + changeOpacity(theme.sidebarText, 0.5), 1); } @@ -607,11 +606,12 @@ export function applyTheme(theme) { } if (theme.centerChannelBg) { - changeCss('.app__content', 'background:' + theme.centerChannelBg, 1); + changeCss('.app__content, .markdown__table, .markdown__table tbody tr', 'background:' + theme.centerChannelBg, 1); changeCss('#post-list .post-list-holder-by-time', 'background:' + theme.centerChannelBg, 1); changeCss('#post-create', 'background:' + theme.centerChannelBg, 1); changeCss('.search-bar__container .search__form .search-bar', 'background:' + theme.centerChannelBg, 1); changeCss('.date-separator .separator__text, .new-separator .separator__text', 'background:' + theme.centerChannelBg, 1); + changeCss('.post-image__column .post-image__details', 'background:' + theme.centerChannelBg, 1); changeCss('.sidebar--right', 'background:' + theme.centerChannelBg, 1); } @@ -619,9 +619,14 @@ export function applyTheme(theme) { changeCss('.app__content', 'color:' + theme.centerChannelColor, 2); changeCss('#post-create', 'color:' + theme.centerChannelColor, 2); changeCss('.channel-header .heading', 'color:' + theme.centerChannelColor, 1); + changeCss('.markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); changeCss('.channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1); changeCss('.channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1); - changeCss('.custom-textarea', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2) + '!important; color: ' + theme.centerChannelColor, 1); + changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); + changeCss('.custom-textarea', 'color:' + theme.centerChannelColor, 1); + changeCss('.post-image__column', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 2); + changeCss('.post-image__column .post-image__details', 'color:' + theme.centerChannelColor, 2); + changeCss('.post-image__column a, .post-image__column a:hover, .post-image__column a:focus', 'color:' + theme.centerChannelColor, 1); changeCss('.search-bar__container .search__form .search-bar', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: ' + theme.centerChannelColor, 2); changeCss('.search-bar__container .search__form', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); changeCss('.channel-intro .channel-intro__content', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1); @@ -629,7 +634,8 @@ export function applyTheme(theme) { changeCss('.date-separator .separator__hr, .new-separator .separator__hr, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); changeCss('.channel-intro', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); - changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); + changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, .post.post--comment.other--root .post-comment, .post.same--root .post-body', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); + changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); changeCss('@media(max-width: 1800px){.inner__wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); changeCss('.post:hover, .sidebar--right .sidebar--right__header', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); |