diff options
Diffstat (limited to 'webapp')
126 files changed, 4656 insertions, 4327 deletions
diff --git a/webapp/Makefile b/webapp/Makefile index 4cc9be1d3..6ec75d1df 100644 --- a/webapp/Makefile +++ b/webapp/Makefile @@ -22,6 +22,11 @@ run: .npminstall npm run run & +run-fullmap: .npminstall + @echo FULL SOURCE MAP Running mattermost Webapp for development FULL SOURCE MAP + + npm run run-fullmap & + stop: @echo Stopping changes watching diff --git a/webapp/action_creators/global_actions.jsx b/webapp/action_creators/global_actions.jsx index ab38532a6..9c38d8955 100644 --- a/webapp/action_creators/global_actions.jsx +++ b/webapp/action_creators/global_actions.jsx @@ -13,23 +13,56 @@ import * as Utils from 'utils/utils.jsx'; import * as Websockets from './websocket_actions.jsx'; import * as I18n from 'i18n/i18n.jsx'; +import {browserHistory} from 'react-router'; + import en from 'i18n/en.json'; export function emitChannelClickEvent(channel) { - AsyncClient.getChannels(true); - AsyncClient.getChannelExtraInfo(channel.id); - AsyncClient.updateLastViewedAt(channel.id); - AsyncClient.getPosts(channel.id); + function userVisitedFakeChannel(chan, success, fail) { + const otherUserId = Utils.getUserIdFromChannelName(chan); + Client.createDirectChannel( + chan, + otherUserId, + (data) => { + success(data); + }, + () => { + fail(); + } + ); + } + function switchToChannel(chan) { + AsyncClient.getChannels(true); + AsyncClient.getChannelExtraInfo(chan.id); + AsyncClient.updateLastViewedAt(chan.id); + AsyncClient.getPosts(chan.id); + Client.trackPage(); + + AppDispatcher.handleViewAction({ + type: ActionTypes.CLICK_CHANNEL, + name: chan.name, + id: chan.id, + prev: ChannelStore.getCurrentId() + }); + } - AppDispatcher.handleViewAction({ - type: ActionTypes.CLICK_CHANNEL, - name: channel.name, - id: channel.id, - prev: ChannelStore.getCurrentId() - }); + if (channel.fake) { + userVisitedFakeChannel( + channel, + (data) => { + switchToChannel(data); + }, + () => { + browserHistory.push('/' + this.state.currentTeam.name); + } + ); + } else { + switchToChannel(channel); + } } export function emitPostFocusEvent(postId) { + AsyncClient.getChannels(true); Client.getPostById( postId, (data) => { @@ -39,6 +72,8 @@ export function emitPostFocusEvent(postId) { post_list: data }); + AsyncClient.getChannelExtraInfo(data.channel_id); + AsyncClient.getPostsBefore(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS); AsyncClient.getPostsAfter(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS); } @@ -264,6 +299,15 @@ export function newLocalizationSelected(locale) { } } +export function loadBrowserLocale() { + let locale = (navigator.languages && navigator.languages.length > 0 ? navigator.languages[0] : + (navigator.language || navigator.userLanguage)).split('-')[0]; + if (!I18n.getLanguages()[locale]) { + locale = 'en'; + } + return newLocalizationSelected(locale); +} + export function viewLoggedIn() { AsyncClient.getChannels(); AsyncClient.getChannelExtraInfo(); @@ -291,3 +335,4 @@ export function emitRemoteUserTypingEvent(channelId, userId, postParentId) { postParentId }); } + diff --git a/webapp/action_creators/websocket_actions.jsx b/webapp/action_creators/websocket_actions.jsx index 611d53bf7..a66d79d18 100644 --- a/webapp/action_creators/websocket_actions.jsx +++ b/webapp/action_creators/websocket_actions.jsx @@ -7,6 +7,7 @@ import PostStore from 'stores/post_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import BrowserStore from 'stores/browser_store.jsx'; import ErrorStore from 'stores/error_store.jsx'; +import NotificationStore from 'stores/notification_store.jsx'; //eslint-disable-line no-unused-vars import * as Utils from 'utils/utils.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; @@ -21,6 +22,7 @@ const WEBSOCKET_RETRY_TIME = 3000; var conn = null; var connectFailCount = 0; var pastFirstInit = false; +var manuallyClosed = false; export function initialize() { if (window.WebSocket && !conn) { @@ -35,6 +37,8 @@ export function initialize() { console.log('websocket connecting to ' + connUrl); //eslint-disable-line no-console } + manuallyClosed = false; + conn = new WebSocket(connUrl); conn.onopen = () => { @@ -63,18 +67,20 @@ export function initialize() { connectFailCount = connectFailCount + 1; if (connectFailCount > MAX_WEBSOCKET_FAILS) { - ErrorStore.storeLastError(Utils.localizeMessage('channel_loader.socketError', 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.')); + ErrorStore.storeLastError({message: Utils.localizeMessage('channel_loader.socketError', 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.')}); } ErrorStore.setConnectionErrorCount(connectFailCount); ErrorStore.emitChange(); - setTimeout( - () => { - initialize(); - }, - WEBSOCKET_RETRY_TIME - ); + if (!manuallyClosed) { + setTimeout( + () => { + initialize(); + }, + WEBSOCKET_RETRY_TIME + ); + } }; conn.onerror = (evt) => { @@ -147,6 +153,7 @@ export function sendMessage(msg) { } export function close() { + manuallyClosed = true; if (conn && conn.readyState === WebSocket.OPEN) { conn.close(); } diff --git a/webapp/components/about_build_modal.jsx b/webapp/components/about_build_modal.jsx index e73d842d0..4fd946401 100644 --- a/webapp/components/about_build_modal.jsx +++ b/webapp/components/about_build_modal.jsx @@ -6,6 +6,7 @@ import {Modal} from 'react-bootstrap'; import {FormattedMessage} from 'react-intl'; import React from 'react'; +import Constants from 'utils/constants.jsx'; export default class AboutBuildModal extends React.Component { constructor(props) { @@ -20,6 +21,7 @@ export default class AboutBuildModal extends React.Component { render() { const config = global.window.mm_config; const license = global.window.mm_license; + const mattermostLogo = Constants.MATTERMOST_ICON_SVG; let title = ( <FormattedMessage @@ -28,6 +30,28 @@ export default class AboutBuildModal extends React.Component { /> ); + let subTitle = ( + <FormattedMessage + id='about.teamEditionSt' + defaultMessage='All your team communication in one place, instantly searchable and accessible anywhere.' + /> + ); + + let learnMore = ( + <div> + <FormattedMessage + id='about.teamEditionLearn' + defaultMessage='Join the Mattermost community at ' + /> + <a + target='_blank' + href='http://www.mattermost.org/' + > + {'mattermost.org'} + </a> + </div> + ); + let licensee; if (config.BuildEnterpriseReady === 'true') { title = ( @@ -36,6 +60,29 @@ export default class AboutBuildModal extends React.Component { defaultMessage='Enterprise Edition' /> ); + + subTitle = ( + <FormattedMessage + id='about.enterpriseEditionSt' + defaultMessage='Modern enterprise communication from behind your firewall.' + /> + ); + + learnMore = ( + <div> + <FormattedMessage + id='about.enterpriseEditionLearn' + defaultMessage='Learn more about Enterprise Edition at ' + /> + <a + target='_blank' + href='http://about.mattermost.com/' + > + {'about.mattermost.com'} + </a> + </div> + ); + if (license.IsLicensed === 'true') { title = ( <FormattedMessage @@ -44,14 +91,12 @@ export default class AboutBuildModal extends React.Component { /> ); licensee = ( - <div className='row form-group'> - <div className='col-sm-3 info__label'> - <FormattedMessage - id='about.licensed' - defaultMessage='Licensed by:' - /> - </div> - <div className='col-sm-9'>{license.Company}</div> + <div className='form-group'> + <FormattedMessage + id='about.licensed' + defaultMessage='Licensed by:' + /> + {license.Company} </div> ); } @@ -59,6 +104,7 @@ export default class AboutBuildModal extends React.Component { return ( <Modal + dialogClassName='about-modal' show={this.props.show} onHide={this.doHide} > @@ -71,57 +117,54 @@ export default class AboutBuildModal extends React.Component { </Modal.Title> </Modal.Header> <Modal.Body> - <h4>{'Mattermost'} {title}</h4> - {licensee} - <div className='row form-group'> - <div className='col-sm-3 info__label'> - <FormattedMessage - id='about.version' - defaultMessage='Version:' + <div className='about-modal__content'> + <div className='about-modal__logo'> + <span + className='icon' + dangerouslySetInnerHTML={{__html: mattermostLogo}} /> </div> - <div className='col-sm-9'>{config.Version}</div> - </div> - <div className='row form-group'> - <div className='col-sm-3 info__label'> - <FormattedMessage - id='about.number' - defaultMessage='Build Number:' - /> + <div> + <h3 className='about-modal__title'>{'Mattermost'} {title}</h3> + <p className='about-modal__subtitle padding-bottom'>{subTitle}</p> + <div className='form-group less'> + <div> + <FormattedMessage + id='about.version' + defaultMessage='Version:' + /> + {config.Version} ({config.BuildNumber}) + </div> + </div> + {licensee} </div> - <div className='col-sm-9'>{config.BuildNumber}</div> </div> - <div className='row form-group'> - <div className='col-sm-3 info__label'> + <div className='about-modal__footer'> + {learnMore} + <div className='form-group about-modal__copyright'> <FormattedMessage - id='about.date' - defaultMessage='Build Date:' + id='about.copyright' + defaultMessage='Copyright 2016 Mattermost, Inc. All rights reserved' /> </div> - <div className='col-sm-9'>{config.BuildDate}</div> </div> - <div className='row form-group'> - <div className='col-sm-3 info__label'> + <div className='about-modal__hash form-group padding-top x2'> + <p> <FormattedMessage id='about.hash' defaultMessage='Build Hash:' /> - </div> - <div className='col-sm-9'>{config.BuildHash}</div> + {config.BuildHash} + </p> + <p> + <FormattedMessage + id='about.date' + defaultMessage='Build Date:' + /> + {config.BuildDate} + </p> </div> </Modal.Body> - <Modal.Footer> - <button - type='button' - className='btn btn-default' - onClick={this.doHide} - > - <FormattedMessage - id='about.close' - defaultMessage='Close' - /> - </button> - </Modal.Footer> </Modal> ); } diff --git a/webapp/components/admin_console/admin_navbar_dropdown.jsx b/webapp/components/admin_console/admin_navbar_dropdown.jsx index 527f97959..729d4b14d 100644 --- a/webapp/components/admin_console/admin_navbar_dropdown.jsx +++ b/webapp/components/admin_console/admin_navbar_dropdown.jsx @@ -64,7 +64,7 @@ export default class AdminNavbarDropdown extends React.Component { > <li> <Link - to={Utils.getWindowLocationOrigin() + '/' + this.state.currentTeam.name + '/channels/town-square'} + to={'/' + this.state.currentTeam.name + '/channels/town-square'} > <FormattedMessage id='admin.nav.switch' diff --git a/webapp/components/admin_console/compliance_settings.jsx b/webapp/components/admin_console/compliance_settings.jsx index fb2ae26f9..206bb0faa 100644 --- a/webapp/components/admin_console/compliance_settings.jsx +++ b/webapp/components/admin_console/compliance_settings.jsx @@ -223,7 +223,7 @@ export default class ComplianceSettings extends React.Component { </label> <p className='help-text'> <FormattedMessage - id='admin.compliance.enableDesc' + id='admin.compliance.enableDailyDesc' defaultMessage='When true, Mattermost will generate a daily compliance report.' /> </p> diff --git a/webapp/components/admin_console/service_settings.jsx b/webapp/components/admin_console/service_settings.jsx index 881d22d76..2c3f4081c 100644 --- a/webapp/components/admin_console/service_settings.jsx +++ b/webapp/components/admin_console/service_settings.jsx @@ -87,6 +87,10 @@ class ServiceSettings extends React.Component { config.ServiceSettings.EnableCommands = ReactDOM.findDOMNode(this.refs.EnableCommands).checked; config.ServiceSettings.EnableOnlyAdminIntegrations = ReactDOM.findDOMNode(this.refs.EnableOnlyAdminIntegrations).checked; + if (this.refs.EnableMultifactorAuthentication) { + config.ServiceSettings.EnableMultifactorAuthentication = ReactDOM.findDOMNode(this.refs.EnableMultifactorAuthentication).checked; + } + //config.ServiceSettings.EnableOAuthServiceProvider = ReactDOM.findDOMNode(this.refs.EnableOAuthServiceProvider).checked; var MaximumLoginAttempts = DefaultMaximumLoginAttempts; @@ -173,6 +177,58 @@ class ServiceSettings extends React.Component { saveClass = 'btn btn-primary'; } + let mfaSetting; + if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MFA === 'true') { + mfaSetting = ( + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='EnableMultifactorAuthentication' + > + <FormattedMessage + id='admin.service.mfaTitle' + defaultMessage='Enable Multi-factor Authentication:' + /> + </label> + <div className='col-sm-8'> + <label className='radio-inline'> + <input + type='radio' + name='EnableMultifactorAuthentication' + value='true' + ref='EnableMultifactorAuthentication' + defaultChecked={this.props.config.ServiceSettings.EnableMultifactorAuthentication} + onChange={this.handleChange} + /> + <FormattedMessage + id='admin.service.true' + defaultMessage='true' + /> + </label> + <label className='radio-inline'> + <input + type='radio' + name='EnableMultifactorAuthentication' + value='false' + defaultChecked={!this.props.config.ServiceSettings.EnableMultifactorAuthentication} + onChange={this.handleChange} + /> + <FormattedMessage + id='admin.service.false' + defaultMessage='false' + /> + </label> + <p className='help-text'> + <FormattedMessage + id='admin.service.mfaDesc' + defaultMessage='When true, users will be given the option to add multi-factor authentication to their account. They will need a smartphone and an authenticator app such as Google Authenticator.' + /> + </p> + </div> + </div> + ); + } + return ( <div className='wrapper--fixed'> @@ -773,6 +829,8 @@ class ServiceSettings extends React.Component { </div> </div> + {mfaSetting} + <div className='form-group'> <label className='control-label col-sm-4' diff --git a/webapp/components/admin_console/user_item.jsx b/webapp/components/admin_console/user_item.jsx index 91f567d4d..c00050584 100644 --- a/webapp/components/admin_console/user_item.jsx +++ b/webapp/components/admin_console/user_item.jsx @@ -333,7 +333,7 @@ export default class UserItem extends React.Component { <div> <FormattedMessage id='admin.user_item.confirmDemoteDescription' - defaultMessage="If you demote yourself from the System Admin role and there is not another user with System Admin privileges, you\'ll need to re-assign a System Admin by accessing the Mattermost server through a terminal and running the following command." + defaultMessage="If you demote yourself from the System Admin role and there is not another user with System Admin privileges, you'll need to re-assign a System Admin by accessing the Mattermost server through a terminal and running the following command." /> <br/> <br/> diff --git a/webapp/components/analytics/team_analytics.jsx b/webapp/components/analytics/team_analytics.jsx index efc965f24..9b4eb1f94 100644 --- a/webapp/components/analytics/team_analytics.jsx +++ b/webapp/components/analytics/team_analytics.jsx @@ -154,7 +154,7 @@ class TeamAnalytics extends React.Component { <TableChart title={ <FormattedMessage - id='analytics.team.activeUsers' + id='analytics.team.recentUsers' defaultMessage='Recent Active Users' /> } diff --git a/webapp/components/backstage/add_incoming_webhook.jsx b/webapp/components/backstage/add_incoming_webhook.jsx new file mode 100644 index 000000000..83027c6b3 --- /dev/null +++ b/webapp/components/backstage/add_incoming_webhook.jsx @@ -0,0 +1,198 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import {browserHistory} from 'react-router'; + +import ChannelSelect from 'components/channel_select.jsx'; +import {FormattedMessage} from 'react-intl'; +import FormError from 'components/form_error.jsx'; +import {Link} from 'react-router'; +import SpinnerButton from 'components/spinner_button.jsx'; + +export default class AddIncomingWebhook extends React.Component { + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + + this.updateName = this.updateName.bind(this); + this.updateDescription = this.updateDescription.bind(this); + this.updateChannelId = this.updateChannelId.bind(this); + + this.state = { + name: '', + description: '', + channelId: '', + saving: false, + serverError: '', + clientError: null + }; + } + + handleSubmit(e) { + e.preventDefault(); + + if (this.state.saving) { + return; + } + + this.setState({ + saving: true, + serverError: '', + clientError: '' + }); + + if (!this.state.channelId) { + this.setState({ + saving: false, + clientError: ( + <FormattedMessage + id='add_incoming_webhook.channelRequired' + defaultMessage='A valid channel is required' + /> + ) + }); + + return; + } + + const hook = { + channel_id: this.state.channelId + }; + + AsyncClient.addIncomingHook( + hook, + () => { + browserHistory.push('/settings/integrations/installed'); + }, + (err) => { + this.setState({ + serverError: err.message + }); + } + ); + } + + updateName(e) { + this.setState({ + name: e.target.value + }); + } + + updateDescription(e) { + this.setState({ + description: e.target.value + }); + } + + updateChannelId(e) { + this.setState({ + channelId: e.target.value + }); + } + + render() { + return ( + <div className='backstage-content row'> + <div className='add-incoming-webhook'> + <div className='backstage-header'> + <h1> + <FormattedMessage + id='add_incoming_webhook.header' + defaultMessage='Add Incoming Webhook' + /> + </h1> + </div> + </div> + <div className='backstage-form'> + <form className='form-horizontal'> + <div className='form-group'> + <label + className='control-label col-sm-3' + htmlFor='name' + > + <FormattedMessage + id='add_incoming_webhook.name' + defaultMessage='Name' + /> + </label> + <div className='col-md-5 col-sm-9'> + <input + id='name' + type='text' + className='form-control' + value={this.state.name} + onChange={this.updateName} + /> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-3' + htmlFor='description' + > + <FormattedMessage + id='add_incoming_webhook.description' + defaultMessage='Description' + /> + </label> + <div className='col-md-5 col-sm-9'> + <input + id='description' + type='text' + className='form-control' + value={this.state.description} + onChange={this.updateDescription} + /> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-3' + htmlFor='channelId' + > + <FormattedMessage + id='add_incoming_webhook.channel' + defaultMessage='Channel' + /> + </label> + <div className='col-md-5 col-sm-9'> + <ChannelSelect + id='channelId' + value={this.state.channelId} + onChange={this.updateChannelId} + /> + </div> + </div> + <div className='backstage-form__footer'> + <FormError errors={[this.state.serverError, this.state.clientError]}/> + <Link + className='btn btn-sm' + to={'/settings/integrations/add'} + > + <FormattedMessage + id='add_incoming_webhook.cancel' + defaultMessage='Cancel' + /> + </Link> + <SpinnerButton + className='btn btn-primary' + type='submit' + spinning={this.state.saving} + onClick={this.handleSubmit} + > + <FormattedMessage + id='add_incoming_webhook.save' + defaultMessage='Save' + /> + </SpinnerButton> + </div> + </form> + </div> + </div> + ); + } +} diff --git a/webapp/components/backstage/add_integration.jsx b/webapp/components/backstage/add_integration.jsx new file mode 100644 index 000000000..5f4a69bfe --- /dev/null +++ b/webapp/components/backstage/add_integration.jsx @@ -0,0 +1,76 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import {FormattedMessage} from 'react-intl'; +import AddIntegrationOption from './add_integration_option.jsx'; + +import WebhookIcon from 'images/webhook_icon.jpg'; + +export default class AddIntegration extends React.Component { + render() { + const options = []; + + if (window.mm_config.EnableIncomingWebhooks === 'true') { + options.push( + <AddIntegrationOption + key='incomingWebhook' + image={WebhookIcon} + title={ + <FormattedMessage + id='add_integration.incomingWebhook.title' + defaultMessage='Incoming Webhook' + /> + } + description={ + <FormattedMessage + id='add_integration.incomingWebhook.description' + defaultMessage='Create webhook URLs for use in external integrations.' + /> + } + link={'/settings/integrations/add/incoming_webhook'} + /> + ); + } + + if (window.mm_config.EnableOutgoingWebhooks === 'true') { + options.push( + <AddIntegrationOption + key='outgoingWebhook' + image={WebhookIcon} + title={ + <FormattedMessage + id='add_integration.outgoingWebhook.title' + defaultMessage='Outgoing Webhook' + /> + } + description={ + <FormattedMessage + id='add_integration.outgoingWebhook.description' + defaultMessage='Create webhooks to send new message events to an external integration.' + /> + } + link={'/settings/integrations/add/outgoing_webhook'} + /> + ); + } + + return ( + <div className='backstage-content row'> + <div className='backstage-header'> + <h1> + <FormattedMessage + id='add_integration.header' + defaultMessage='Add Integration' + /> + </h1> + </div> + <div> + {options} + </div> + </div> + ); + } +} + diff --git a/webapp/components/backstage/add_integration_option.jsx b/webapp/components/backstage/add_integration_option.jsx new file mode 100644 index 000000000..b17ebb185 --- /dev/null +++ b/webapp/components/backstage/add_integration_option.jsx @@ -0,0 +1,39 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import {Link} from 'react-router'; + +export default class AddIntegrationOption extends React.Component { + static get propTypes() { + return { + image: React.PropTypes.string.isRequired, + title: React.PropTypes.node.isRequired, + description: React.PropTypes.node.isRequired, + link: React.PropTypes.string.isRequired + }; + } + + render() { + const {image, title, description, link} = this.props; + + return ( + <Link + to={link} + className='add-integration' + > + <img + className='add-integration__image' + src={image} + /> + <div className='add-integration__title'> + {title} + </div> + <div className='add-integration__description'> + {description} + </div> + </Link> + ); + } +} diff --git a/webapp/components/backstage/add_outgoing_webhook.jsx b/webapp/components/backstage/add_outgoing_webhook.jsx new file mode 100644 index 000000000..5d98138df --- /dev/null +++ b/webapp/components/backstage/add_outgoing_webhook.jsx @@ -0,0 +1,270 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import {browserHistory} from 'react-router'; + +import ChannelSelect from 'components/channel_select.jsx'; +import {FormattedMessage} from 'react-intl'; +import FormError from 'components/form_error.jsx'; +import {Link} from 'react-router'; +import SpinnerButton from 'components/spinner_button.jsx'; + +export default class AddOutgoingWebhook extends React.Component { + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + + this.updateName = this.updateName.bind(this); + this.updateDescription = this.updateDescription.bind(this); + this.updateChannelId = this.updateChannelId.bind(this); + this.updateTriggerWords = this.updateTriggerWords.bind(this); + this.updateCallbackUrls = this.updateCallbackUrls.bind(this); + + this.state = { + name: '', + description: '', + channelId: '', + triggerWords: '', + callbackUrls: '', + saving: false, + serverError: '', + clientError: null + }; + } + + handleSubmit(e) { + e.preventDefault(); + + if (this.state.saving) { + return; + } + + this.setState({ + saving: true, + serverError: '', + clientError: '' + }); + + if (!this.state.channelId && !this.state.triggerWords) { + this.setState({ + saving: false, + clientError: ( + <FormattedMessage + id='add_outgoing_webhook.triggerWordsOrChannelRequired' + defaultMessage='A valid channel or a list of trigger words is required' + /> + ) + }); + + return; + } + + if (!this.state.callbackUrls) { + this.setState({ + saving: false, + clientError: ( + <FormattedMessage + id='add_outgoing_webhook.callbackUrlsRequired' + defaultMessage='One or more callback URLs are required' + /> + ) + }); + + return; + } + + const hook = { + channel_id: this.state.channelId, + trigger_words: this.state.triggerWords.split('\n').map((word) => word.trim()), + callback_urls: this.state.callbackUrls.split('\n').map((url) => url.trim()) + }; + + AsyncClient.addOutgoingHook( + hook, + () => { + browserHistory.push('/settings/integrations/installed'); + }, + (err) => { + this.setState({ + serverError: err.message + }); + } + ); + } + + updateName(e) { + this.setState({ + name: e.target.value + }); + } + + updateDescription(e) { + this.setState({ + description: e.target.value + }); + } + + updateChannelId(e) { + this.setState({ + channelId: e.target.value + }); + } + + updateTriggerWords(e) { + this.setState({ + triggerWords: e.target.value + }); + } + + updateCallbackUrls(e) { + this.setState({ + callbackUrls: e.target.value + }); + } + + render() { + return ( + <div className='backstage-content row'> + <div className='add-outgoing-webhook'> + <div className='backstage-header'> + <h1> + <FormattedMessage + id='add_outgoing_webhook.header' + defaultMessage='Add Outgoing Webhook' + /> + </h1> + </div> + </div> + <div className='backstage-form'> + <form className='form-horizontal'> + <div className='form-group'> + <label + className='control-label col-sm-3' + htmlFor='name' + > + <FormattedMessage + id='add_outgoing_webhook.name' + defaultMessage='Name' + /> + </label> + <div className='col-md-5 col-sm-9'> + <input + id='name' + type='text' + className='form-control' + value={this.state.name} + onChange={this.updateName} + /> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-3' + htmlFor='description' + > + <FormattedMessage + id='add_outgoing_webhook.description' + defaultMessage='Description' + /> + </label> + <div className='col-md-5 col-sm-9'> + <input + id='description' + type='text' + className='form-control' + value={this.state.description} + onChange={this.updateDescription} + /> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-3' + htmlFor='channelId' + > + <FormattedMessage + id='add_outgoing_webhook.channel' + defaultMessage='Channel' + /> + </label> + <div className='col-md-5 col-sm-9'> + <ChannelSelect + id='channelId' + value={this.state.channelId} + onChange={this.updateChannelId} + /> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-3' + htmlFor='triggerWords' + > + <FormattedMessage + id='add_outgoing_webhook.triggerWords' + defaultMessage='Trigger Words (One Per Line)' + /> + </label> + <div className='col-md-5 col-sm-9'> + <textarea + id='triggerWords' + rows='3' + className='form-control' + value={this.state.triggerWords} + onChange={this.updateTriggerWords} + /> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-3' + htmlFor='callbackUrls' + > + <FormattedMessage + id='add_outgoing_webhook.callbackUrls' + defaultMessage='Callback URLs (One Per Line)' + /> + </label> + <div className='col-md-5 col-sm-9'> + <textarea + id='callbackUrls' + rows='3' + className='form-control' + value={this.state.callbackUrls} + onChange={this.updateCallbackUrls} + /> + </div> + </div> + <div className='backstage-form__footer'> + <FormError errors={[this.state.serverError, this.state.clientError]}/> + <Link + className='btn btn-sm' + to={'/settings/integrations/add'} + > + <FormattedMessage + id='add_outgoing_webhook.cancel' + defaultMessage='Cancel' + /> + </Link> + <SpinnerButton + className='btn btn-primary' + type='submit' + spinning={this.state.saving} + onClick={this.handleSubmit} + > + <FormattedMessage + id='add_outgoing_webhook.save' + defaultMessage='Save' + /> + </SpinnerButton> + </div> + </form> + </div> + </div> + ); + } +} diff --git a/webapp/components/backstage/backstage_category.jsx b/webapp/components/backstage/backstage_category.jsx new file mode 100644 index 000000000..913c7562c --- /dev/null +++ b/webapp/components/backstage/backstage_category.jsx @@ -0,0 +1,68 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import {Link} from 'react-router'; + +export default class BackstageCategory extends React.Component { + static get propTypes() { + return { + name: React.PropTypes.string.isRequired, + title: React.PropTypes.node.isRequired, + icon: React.PropTypes.string.isRequired, + parentLink: React.PropTypes.string, + children: React.PropTypes.arrayOf(React.PropTypes.element) + }; + } + + static get defaultProps() { + return { + parentLink: '', + children: [] + }; + } + + static get contextTypes() { + return { + router: React.PropTypes.object.isRequired + }; + } + + render() { + const {name, title, icon, parentLink, children} = this.props; + + const link = parentLink + '/' + name; + + let clonedChildren = null; + if (children.length > 0 && this.context.router.isActive(link)) { + clonedChildren = ( + <ul className='sections'> + { + React.Children.map(children, (child) => { + return React.cloneElement(child, { + parentLink: link + }); + }) + } + </ul> + ); + } + + return ( + <li className='backstage-sidebar__category'> + <Link + to={link} + className='category-title' + activeClassName='category-title--active' + > + <i className={'fa ' + icon}/> + <span className='category-title__text'> + {title} + </span> + </Link> + {clonedChildren} + </li> + ); + } +} diff --git a/webapp/components/backstage/backstage_navbar.jsx b/webapp/components/backstage/backstage_navbar.jsx new file mode 100644 index 000000000..d1dac6043 --- /dev/null +++ b/webapp/components/backstage/backstage_navbar.jsx @@ -0,0 +1,61 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import TeamStore from 'stores/team_store.jsx'; + +import {FormattedMessage} from 'react-intl'; +import {Link} from 'react-router'; + +export default class BackstageNavbar extends React.Component { + constructor(props) { + super(props); + + this.handleChange = this.handleChange.bind(this); + + this.state = { + team: TeamStore.getCurrent() + }; + } + + componentDidMount() { + TeamStore.addChangeListener(this.handleChange); + } + + componentWillUnmount() { + TeamStore.removeChangeListener(this.handleChange); + } + + handleChange() { + this.setState({ + team: TeamStore.getCurrent() + }); + } + + render() { + if (!this.state.team) { + return null; + } + + return ( + <div className='backstage-navbar row'> + <Link + className='backstage-navbar__back' + to={`/${this.state.team.display_name}/channels/town-square`} + > + <i className='fa fa-angle-left'/> + <span> + <FormattedMessage + id='backstage_navbar.backToMattermost' + defaultMessage='Back to {siteName}' + values={{ + siteName: global.window.mm_config.SiteName + }} + /> + </span> + </Link> + </div> + ); + } +} diff --git a/webapp/components/backstage/backstage_section.jsx b/webapp/components/backstage/backstage_section.jsx new file mode 100644 index 000000000..d6ce2b258 --- /dev/null +++ b/webapp/components/backstage/backstage_section.jsx @@ -0,0 +1,80 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import {Link} from 'react-router'; + +export default class BackstageSection extends React.Component { + static get propTypes() { + return { + name: React.PropTypes.string.isRequired, + title: React.PropTypes.node.isRequired, + parentLink: React.PropTypes.string, + subsection: React.PropTypes.bool, + children: React.PropTypes.arrayOf(React.PropTypes.element) + }; + } + + static get defaultProps() { + return { + parentLink: '', + subsection: false, + children: [] + }; + } + + static get contextTypes() { + return { + router: React.PropTypes.object.isRequired + }; + } + + getLink() { + return this.props.parentLink + '/' + this.props.name; + } + + render() { + const {title, subsection, children} = this.props; + + const link = this.getLink(); + + let clonedChildren = null; + if (children.length > 0) { + clonedChildren = ( + <ul className='subsections'> + { + React.Children.map(children, (child) => { + return React.cloneElement(child, { + parentLink: link, + subsection: true + }); + }) + } + </ul> + ); + } + + let className = 'section'; + if (subsection) { + className = 'subsection'; + } + + return ( + <li className={className}> + <Link + className={`${className}-title`} + activeClassName={`${className}-title--active`} + onlyActiveOnIndex={true} + onClick={this.handleClick} + to={link} + > + <span className={`${className}-title__text`}> + {title} + </span> + </Link> + {clonedChildren} + </li> + ); + } +} diff --git a/webapp/components/backstage/backstage_sidebar.jsx b/webapp/components/backstage/backstage_sidebar.jsx new file mode 100644 index 000000000..13c4f8b50 --- /dev/null +++ b/webapp/components/backstage/backstage_sidebar.jsx @@ -0,0 +1,68 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import BackstageCategory from './backstage_category.jsx'; +import BackstageSection from './backstage_section.jsx'; +import {FormattedMessage} from 'react-intl'; + +export default class BackstageSidebar extends React.Component { + render() { + return ( + <div className='backstage-sidebar'> + <ul> + <BackstageCategory + name='integrations' + parentLink={'/settings'} + icon='fa-link' + title={ + <FormattedMessage + id='backstage_sidebar.integrations' + defaultMessage='Integrations' + /> + } + > + <BackstageSection + name='installed' + title={( + <FormattedMessage + id='backstage_sidebar.integrations.installed' + defaultMessage='Installed Integrations' + /> + )} + /> + <BackstageSection + name='add' + title={( + <FormattedMessage + id='backstage_sidebar.integrations.add' + defaultMessage='Add Integration' + /> + )} + > + <BackstageSection + name='incoming_webhook' + title={( + <FormattedMessage + id='backstage_sidebar.integrations.add.incomingWebhook' + defaultMessage='Incoming Webhook' + /> + )} + /> + <BackstageSection + name='outgoing_webhook' + title={( + <FormattedMessage + id='backstage_sidebar.integrations.add.outgoingWebhook' + defaultMessage='Outgoing Webhook' + /> + )} + /> + </BackstageSection> + </BackstageCategory> + </ul> + </div> + ); + } +} diff --git a/webapp/components/backstage/installed_incoming_webhook.jsx b/webapp/components/backstage/installed_incoming_webhook.jsx new file mode 100644 index 000000000..f65cf6327 --- /dev/null +++ b/webapp/components/backstage/installed_incoming_webhook.jsx @@ -0,0 +1,71 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import ChannelStore from 'stores/channel_store.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import {FormattedMessage} from 'react-intl'; + +export default class InstalledIncomingWebhook extends React.Component { + static get propTypes() { + return { + incomingWebhook: React.PropTypes.object.isRequired, + onDeleteClick: React.PropTypes.func.isRequired + }; + } + + constructor(props) { + super(props); + + this.handleDeleteClick = this.handleDeleteClick.bind(this); + } + + handleDeleteClick(e) { + e.preventDefault(); + + this.props.onDeleteClick(this.props.incomingWebhook); + } + + render() { + const incomingWebhook = this.props.incomingWebhook; + + const channel = ChannelStore.get(incomingWebhook.channel_id); + const channelName = channel ? channel.display_name : 'cannot find channel'; + + return ( + <div className='backstage-list__item'> + <div className='item-details'> + <div className='item-details__row'> + <span className='item-details__name'> + {channelName} + </span> + <span className='item-details__type'> + <FormattedMessage + id='installed_integrations.incomingWebhookType' + defaultMessage='(Incoming Webhook)' + /> + </span> + </div> + <div className='item-details__row'> + <span className='item-details__description'> + {Utils.getWindowLocationOrigin() + '/hooks/' + incomingWebhook.id} + </span> + </div> + </div> + <div className='item-actions'> + <a + href='#' + onClick={this.handleDeleteClick} + > + <FormattedMessage + id='installed_integrations.delete' + defaultMessage='Delete' + /> + </a> + </div> + </div> + ); + } +} diff --git a/webapp/components/backstage/installed_integrations.jsx b/webapp/components/backstage/installed_integrations.jsx new file mode 100644 index 000000000..fe84ae81a --- /dev/null +++ b/webapp/components/backstage/installed_integrations.jsx @@ -0,0 +1,293 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import ChannelStore from 'stores/channel_store.jsx'; +import IntegrationStore from 'stores/integration_store.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import {FormattedMessage} from 'react-intl'; +import InstalledIncomingWebhook from './installed_incoming_webhook.jsx'; +import InstalledOutgoingWebhook from './installed_outgoing_webhook.jsx'; +import {Link} from 'react-router'; + +export default class InstalledIntegrations extends React.Component { + constructor(props) { + super(props); + + this.handleIntegrationChange = this.handleIntegrationChange.bind(this); + this.updateFilter = this.updateFilter.bind(this); + this.updateTypeFilter = this.updateTypeFilter.bind(this); + + this.deleteIncomingWebhook = this.deleteIncomingWebhook.bind(this); + this.regenOutgoingWebhookToken = this.regenOutgoingWebhookToken.bind(this); + this.deleteOutgoingWebhook = this.deleteOutgoingWebhook.bind(this); + + this.state = { + incomingWebhooks: [], + outgoingWebhooks: [], + typeFilter: '', + filter: '' + }; + } + + componentWillMount() { + IntegrationStore.addChangeListener(this.handleIntegrationChange); + + if (window.mm_config.EnableIncomingWebhooks === 'true') { + if (IntegrationStore.hasReceivedIncomingWebhooks()) { + this.setState({ + incomingWebhooks: IntegrationStore.getIncomingWebhooks() + }); + } else { + AsyncClient.listIncomingHooks(); + } + } + + if (window.mm_config.EnableOutgoingWebhooks === 'true') { + if (IntegrationStore.hasReceivedOutgoingWebhooks()) { + this.setState({ + outgoingWebhooks: IntegrationStore.getOutgoingWebhooks() + }); + } else { + AsyncClient.listOutgoingHooks(); + } + } + } + + componentWillUnmount() { + IntegrationStore.removeChangeListener(this.handleIntegrationChange); + } + + handleIntegrationChange() { + this.setState({ + incomingWebhooks: IntegrationStore.getIncomingWebhooks(), + outgoingWebhooks: IntegrationStore.getOutgoingWebhooks() + }); + } + + updateTypeFilter(e, typeFilter) { + e.preventDefault(); + + this.setState({ + typeFilter + }); + } + + updateFilter(e) { + this.setState({ + filter: e.target.value + }); + } + + deleteIncomingWebhook(incomingWebhook) { + AsyncClient.deleteIncomingHook(incomingWebhook.id); + } + + regenOutgoingWebhookToken(outgoingWebhook) { + AsyncClient.regenOutgoingHookToken(outgoingWebhook.id); + } + + deleteOutgoingWebhook(outgoingWebhook) { + AsyncClient.deleteOutgoingHook(outgoingWebhook.id); + } + + renderTypeFilters(incomingWebhooks, outgoingWebhooks) { + const fields = []; + + if (incomingWebhooks.length > 0 || outgoingWebhooks.length > 0) { + let filterClassName = 'filter-sort'; + if (this.state.typeFilter === '') { + filterClassName += ' filter-sort--active'; + } + + fields.push( + <a + key='allFilter' + className={filterClassName} + href='#' + onClick={(e) => this.updateTypeFilter(e, '')} + > + <FormattedMessage + id='installed_integrations.allFilter' + defaultMessage='All ({count})' + values={{ + count: incomingWebhooks.length + outgoingWebhooks.length + }} + /> + </a> + ); + } + + if (incomingWebhooks.length > 0) { + fields.push( + <span + key='incomingWebhooksDivider' + className='divider' + > + {'|'} + </span> + ); + + let filterClassName = 'filter-sort'; + if (this.state.typeFilter === 'incomingWebhooks') { + filterClassName += ' filter-sort--active'; + } + + fields.push( + <a + key='incomingWebhooksFilter' + className={filterClassName} + href='#' + onClick={(e) => this.updateTypeFilter(e, 'incomingWebhooks')} + > + <FormattedMessage + id='installed_integrations.incomingWebhooksFilter' + defaultMessage='Incoming Webhooks ({count})' + values={{ + count: incomingWebhooks.length + }} + /> + </a> + ); + } + + if (outgoingWebhooks.length > 0) { + fields.push( + <span + key='outgoingWebhooksDivider' + className='divider' + > + {'|'} + </span> + ); + + let filterClassName = 'filter-sort'; + if (this.state.typeFilter === 'outgoingWebhooks') { + filterClassName += ' filter-sort--active'; + } + + fields.push( + <a + key='outgoingWebhooksFilter' + className={filterClassName} + href='#' + onClick={(e) => this.updateTypeFilter(e, 'outgoingWebhooks')} + > + <FormattedMessage + id='installed_integrations.outgoingWebhooksFilter' + defaultMessage='Outgoing Webhooks ({count})' + values={{ + count: outgoingWebhooks.length + }} + /> + </a> + ); + } + + return ( + <div className='backstage-filters__sort'> + {fields} + </div> + ); + } + + render() { + const incomingWebhooks = this.state.incomingWebhooks; + const outgoingWebhooks = this.state.outgoingWebhooks; + + const filter = this.state.filter.toLowerCase(); + + const integrations = []; + if (!this.state.typeFilter || this.state.typeFilter === 'incomingWebhooks') { + for (const incomingWebhook of incomingWebhooks) { + if (filter) { + const channel = ChannelStore.get(incomingWebhook.channel_id); + + if (!channel || channel.name.toLowerCase().indexOf(filter) === -1) { + continue; + } + } + + integrations.push( + <InstalledIncomingWebhook + key={incomingWebhook.id} + incomingWebhook={incomingWebhook} + onDeleteClick={this.deleteIncomingWebhook} + /> + ); + } + } + + if (!this.state.typeFilter || this.state.typeFilter === 'outgoingWebhooks') { + for (const outgoingWebhook of outgoingWebhooks) { + if (filter) { + const channel = ChannelStore.get(outgoingWebhook.channel_id); + + if (!channel || channel.name.toLowerCase().indexOf(filter) === -1) { + continue; + } + } + + integrations.push( + <InstalledOutgoingWebhook + key={outgoingWebhook.id} + outgoingWebhook={outgoingWebhook} + onRegenToken={this.regenOutgoingWebhookToken} + onDelete={this.deleteOutgoingWebhook} + /> + ); + } + } + + return ( + <div className='backstage-content row'> + <div className='installed-integrations'> + <div className='backstage-header'> + <h1> + <FormattedMessage + id='installed_integrations.header' + defaultMessage='Installed Integrations' + /> + </h1> + <Link + className='add-integrations-link' + to={'/settings/integrations/add'} + > + <button + type='button' + className='btn btn-primary' + > + <span> + <FormattedMessage + id='installed_integrations.add' + defaultMessage='Add Integration' + /> + </span> + </button> + </Link> + </div> + <div className='backstage-filters'> + {this.renderTypeFilters(this.state.incomingWebhooks, this.state.outgoingWebhooks)} + <div className='backstage-filter__search'> + <i className='fa fa-search'></i> + <input + type='search' + className='form-control' + placeholder={Utils.localizeMessage('installed_integrations.search', 'Search Integrations')} + value={this.state.filter} + onChange={this.updateFilter} + style={{flexGrow: 0, flexShrink: 0}} + /> + </div> + </div> + <div className='backstage-list'> + {integrations} + </div> + </div> + </div> + ); + } +} diff --git a/webapp/components/backstage/installed_outgoing_webhook.jsx b/webapp/components/backstage/installed_outgoing_webhook.jsx new file mode 100644 index 000000000..fee427260 --- /dev/null +++ b/webapp/components/backstage/installed_outgoing_webhook.jsx @@ -0,0 +1,91 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import ChannelStore from 'stores/channel_store.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import {FormattedMessage} from 'react-intl'; + +export default class InstalledOutgoingWebhook extends React.Component { + static get propTypes() { + return { + outgoingWebhook: React.PropTypes.object.isRequired, + onRegenToken: React.PropTypes.func.isRequired, + onDelete: React.PropTypes.func.isRequired + }; + } + + constructor(props) { + super(props); + + this.handleRegenToken = this.handleRegenToken.bind(this); + this.handleDelete = this.handleDelete.bind(this); + } + + handleRegenToken(e) { + e.preventDefault(); + + this.props.onRegenToken(this.props.outgoingWebhook); + } + + handleDelete(e) { + e.preventDefault(); + + this.props.onDelete(this.props.outgoingWebhook); + } + + render() { + const outgoingWebhook = this.props.outgoingWebhook; + + const channel = ChannelStore.get(outgoingWebhook.channel_id); + const channelName = channel ? channel.display_name : 'cannot find channel'; + + return ( + <div className='backstage-list__item'> + <div className='item-details'> + <div className='item-details__row'> + <span className='item-details__name'> + {channelName} + </span> + <span className='item-details__type'> + <FormattedMessage + id='installed_integrations.outgoingWebhookType' + defaultMessage='(Outgoing Webhook)' + /> + </span> + </div> + <div className='item-details__row'> + <span className='item-details__description'> + {Utils.getWindowLocationOrigin() + '/hooks/' + outgoingWebhook.id} + {' - '} + {outgoingWebhook.token} + </span> + </div> + </div> + <div className='actions'> + <a + href='#' + onClick={this.handleRegenToken} + > + <FormattedMessage + id='installed_integrations.regenToken' + defaultMessage='Regen Token' + /> + </a> + {' - '} + <a + href='#' + onClick={this.handleDelete} + > + <FormattedMessage + id='installed_integrations.delete' + defaultMessage='Delete' + /> + </a> + </div> + </div> + ); + } +} diff --git a/webapp/components/center_panel.jsx b/webapp/components/center_panel.jsx deleted file mode 100644 index 62b12c1d2..000000000 --- a/webapp/components/center_panel.jsx +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import TutorialIntroScreens from './tutorial/tutorial_intro_screens.jsx'; -import CreatePost from './create_post.jsx'; -import PostsViewContainer from './posts_view_container.jsx'; -import PostFocusView from './post_focus_view.jsx'; -import ChannelHeader from './channel_header.jsx'; -import Navbar from './navbar.jsx'; -import FileUploadOverlay from './file_upload_overlay.jsx'; - -import PreferenceStore from 'stores/preference_store.jsx'; -import ChannelStore from 'stores/channel_store.jsx'; -import UserStore from 'stores/user_store.jsx'; - -import * as Utils from 'utils/utils.jsx'; - -import {FormattedMessage} from 'react-intl'; - -import Constants from 'utils/constants.jsx'; -const TutorialSteps = Constants.TutorialSteps; -const Preferences = Constants.Preferences; - -import React from 'react'; -import {Link} from 'react-router'; - -export default class CenterPanel extends React.Component { - constructor(props) { - super(props); - - this.getStateFromStores = this.getStateFromStores.bind(this); - this.validState = this.validState.bind(this); - this.onStoresChange = this.onStoresChange.bind(this); - - this.state = this.getStateFromStores(); - } - getStateFromStores() { - const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999); - return { - showTutorialScreens: tutorialStep <= TutorialSteps.INTRO_SCREENS, - showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS, - user: UserStore.getCurrentUser(), - channel: ChannelStore.getCurrent(), - profiles: JSON.parse(JSON.stringify(UserStore.getProfiles())) - }; - } - validState() { - return this.state.user && this.state.channel && this.state.profiles; - } - onStoresChange() { - this.setState(this.getStateFromStores()); - } - componentDidMount() { - PreferenceStore.addChangeListener(this.onStoresChange); - ChannelStore.addChangeListener(this.onStoresChange); - UserStore.addChangeListener(this.onStoresChange); - } - componentWillUnmount() { - PreferenceStore.removeChangeListener(this.onStoresChange); - ChannelStore.removeChangeListener(this.onStoresChange); - UserStore.removeChangeListener(this.onStoresChange); - } - render() { - if (!this.validState()) { - return null; - } - const channel = this.state.channel; - var handleClick = null; - let postsContainer; - let createPost; - if (this.state.showTutorialScreens) { - postsContainer = <TutorialIntroScreens/>; - createPost = null; - } else if (this.state.showPostFocus) { - postsContainer = <PostFocusView profiles={this.state.profiles}/>; - - handleClick = function clickHandler(e) { - e.preventDefault(); - Utils.switchChannel(channel); - }; - - createPost = ( - <div - id='archive-link-home' - onClick={handleClick} - > - <Link to=''> - <FormattedMessage - id='center_panel.recent' - defaultMessage='Click here to jump to recent messages. ' - /> - <i className='fa fa-arrow-down'></i> - </Link> - </div> - ); - } else { - postsContainer = <PostsViewContainer profiles={this.state.profiles}/>; - createPost = ( - <div - className='post-create__container' - id='post-create' - > - <CreatePost/> - </div> - ); - } - - return ( - <div className='inner-wrap channel__wrap'> - <div className='row header'> - <div id='navbar'> - <Navbar/> - </div> - </div> - <div className='row main'> - <FileUploadOverlay - id='file_upload_overlay' - overlayType='center' - /> - <div - id='app-content' - className='app__content' - > - <div - id='channel-header' - className='channel-header' - > - <ChannelHeader - user={this.state.user} - /> - </div> - {postsContainer} - {createPost} - </div> - </div> - </div> - ); - } -} - -CenterPanel.defaultProps = { -}; - -CenterPanel.propTypes = { -}; diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx index 369fa2dbb..482aabc01 100644 --- a/webapp/components/channel_header.jsx +++ b/webapp/components/channel_header.jsx @@ -26,6 +26,7 @@ import * as Utils from 'utils/utils.jsx'; import * as TextFormatting from 'utils/text_formatting.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import * as Client from 'utils/client.jsx'; +import * as GlobalActions from 'action_creators/global_actions.jsx'; import Constants from 'utils/constants.jsx'; import {FormattedMessage} from 'react-intl'; @@ -53,11 +54,11 @@ export default class ChannelHeader extends React.Component { this.state = state; } getStateFromStores() { - const extraInfo = ChannelStore.getCurrentExtraInfo(); + const extraInfo = ChannelStore.getExtraInfo(this.props.channelId); return { - channel: ChannelStore.getCurrent(), - memberChannel: ChannelStore.getCurrentMember(), + channel: ChannelStore.get(this.props.channelId), + memberChannel: ChannelStore.getMember(this.props.channelId), users: extraInfo.members, userCount: extraInfo.member_count, searchVisible: SearchStore.getSearchResults() !== null, @@ -105,7 +106,7 @@ export default class ChannelHeader extends React.Component { }); const townsquare = ChannelStore.getByName('town-square'); - Utils.switchChannel(townsquare); + GlobalActions.emitChannelClickEvent(townsquare); }, (err) => { AsyncClient.dispatchError(err, 'handleLeave'); @@ -215,9 +216,9 @@ export default class ChannelHeader extends React.Component { if (!isDirect) { popoverListMembers = ( <PopoverListMembers + channel={channel} members={this.state.users} memberCount={this.state.userCount} - channelId={channel.id} /> ); } @@ -433,7 +434,10 @@ export default class ChannelHeader extends React.Component { } return ( - <div> + <div + id='channel-header' + className='channel-header' + > <table className='channel-header alt'> <tbody> <tr> @@ -518,4 +522,5 @@ export default class ChannelHeader extends React.Component { } ChannelHeader.propTypes = { + channelId: React.PropTypes.string.isRequired }; diff --git a/webapp/components/channel_invite_button.jsx b/webapp/components/channel_invite_button.jsx index e4af9f9ce..1fcd461ea 100644 --- a/webapp/components/channel_invite_button.jsx +++ b/webapp/components/channel_invite_button.jsx @@ -65,6 +65,7 @@ export default class ChannelInviteButton extends React.Component { render() { return ( <SpinnerButton + className='btn btn-sm btn-primary' onClick={this.handleClick} spinning={this.state.addingUser} > diff --git a/webapp/components/channel_notifications_modal.jsx b/webapp/components/channel_notifications_modal.jsx index cc1162b77..564776876 100644 --- a/webapp/components/channel_notifications_modal.jsx +++ b/webapp/components/channel_notifications_modal.jsx @@ -1,6 +1,7 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import $ from 'jquery'; import {Modal} from 'react-bootstrap'; import SettingItemMin from './setting_item_min.jsx'; import SettingItemMax from './setting_item_max.jsx'; @@ -33,6 +34,7 @@ export default class ChannelNotificationsModal extends React.Component { }; } updateSection(section) { + $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update'); this.setState({activeSection: section}); } componentWillReceiveProps(nextProps) { diff --git a/webapp/components/channel_select.jsx b/webapp/components/channel_select.jsx new file mode 100644 index 000000000..8622d1f57 --- /dev/null +++ b/webapp/components/channel_select.jsx @@ -0,0 +1,79 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import Constants from 'utils/constants.jsx'; +import ChannelStore from 'stores/channel_store.jsx'; +import * as Utils from 'utils/utils.jsx'; + +export default class ChannelSelect extends React.Component { + static get propTypes() { + return { + onChange: React.PropTypes.func, + value: React.PropTypes.string + }; + } + + constructor(props) { + super(props); + + this.handleChannelChange = this.handleChannelChange.bind(this); + + this.state = { + channels: [] + }; + } + + componentWillMount() { + this.setState({ + channels: ChannelStore.getAll() + }); + + ChannelStore.addChangeListener(this.handleChannelChange); + } + + componentWillUnmount() { + ChannelStore.removeChangeListener(this.handleChannelChange); + } + + handleChannelChange() { + this.setState({ + channels: ChannelStore.getAll() + }); + } + + render() { + const options = [ + <option + key='' + value='' + > + {Utils.localizeMessage('channel_select.placeholder', '--- Select a channel ---')} + </option> + ]; + + this.state.channels.forEach((channel) => { + if (channel.type !== Constants.DM_CHANNEL) { + options.push( + <option + key={channel.id} + value={channel.id} + > + {channel.display_name} + </option> + ); + } + }); + + return ( + <select + className='form-control' + value={this.props.value} + onChange={this.props.onChange} + > + {options} + </select> + ); + } +} diff --git a/webapp/components/channel_view.jsx b/webapp/components/channel_view.jsx index 34e1666d0..4cca5aa98 100644 --- a/webapp/components/channel_view.jsx +++ b/webapp/components/channel_view.jsx @@ -1,14 +1,63 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import CenterPanel from 'components/center_panel.jsx'; - import React from 'react'; +import ChannelHeader from 'components/channel_header.jsx'; +import PostsViewContainer from 'components/posts_view_container.jsx'; +import CreatePost from 'components/create_post.jsx'; + +import ChannelStore from 'stores/channel_store.jsx'; + export default class ChannelView extends React.Component { + constructor(props) { + super(props); + + this.getStateFromStores = this.getStateFromStores.bind(this); + this.isStateValid = this.isStateValid.bind(this); + this.updateState = this.updateState.bind(this); + + this.state = this.getStateFromStores(props); + } + getStateFromStores(props) { + const channel = ChannelStore.getByName(props.params.channel); + const channelId = channel ? channel.id : ''; + return { + channelId + }; + } + isStateValid() { + return this.state.channelId !== ''; + } + updateState() { + this.setState(this.getStateFromStores(this.props)); + } + componentDidMount() { + ChannelStore.addChangeListener(this.updateState); + } + componentWillUnmount() { + ChannelStore.removeChangeListener(this.updateState); + } + componentWillReceiveProps(nextProps) { + this.setState(this.getStateFromStores(nextProps)); + } render() { return ( - <CenterPanel/> + <div + id='app-content' + className='app__content' + > + <ChannelHeader + channelId={this.state.channelId} + /> + <PostsViewContainer profiles={this.props.profiles}/> + <div + className='post-create__container' + id='post-create' + > + <CreatePost/> + </div> + </div> ); } } @@ -16,5 +65,6 @@ ChannelView.defaultProps = { }; ChannelView.propTypes = { - params: React.PropTypes.object + params: React.PropTypes.object.isRequired, + profiles: React.PropTypes.object }; diff --git a/webapp/components/claim/components/email_to_ldap.jsx b/webapp/components/claim/components/email_to_ldap.jsx index 1f51f9cd5..1ceb42a27 100644 --- a/webapp/components/claim/components/email_to_ldap.jsx +++ b/webapp/components/claim/components/email_to_ldap.jsx @@ -21,7 +21,7 @@ export default class EmailToLDAP extends React.Component { e.preventDefault(); var state = {}; - const password = ReactDOM.findDOMNode(this.refs.password).value.trim(); + const password = ReactDOM.findDOMNode(this.refs.emailpassword).value.trim(); if (!password) { state.error = Utils.localizeMessage('claim.email_to_ldap.pwdError', 'Please enter your password.'); this.setState(state); @@ -105,12 +105,18 @@ export default class EmailToLDAP extends React.Component { }} /> </p> + <input + type='text' + style={{display: 'none'}} + name='fakeusernameremembered' + /> <div className={formClass}> <input type='password' className='form-control' - name='password' - ref='password' + name='emailPassword' + ref='emailpassword' + autoComplete='off' placeholder={Utils.localizeMessage('claim.email_to_ldap.pwd', 'Password')} spellCheck='false' /> @@ -131,6 +137,7 @@ export default class EmailToLDAP extends React.Component { className='form-control' name='ldapId' ref='ldapid' + autoComplete='off' placeholder={Utils.localizeMessage('claim.email_to_ldap.ldapId', 'LDAP ID')} spellCheck='false' /> @@ -141,6 +148,7 @@ export default class EmailToLDAP extends React.Component { className='form-control' name='ldapPassword' ref='ldappassword' + autoComplete='off' placeholder={Utils.localizeMessage('claim.email_to_ldap.ldapPwd', 'LDAP Password')} spellCheck='false' /> diff --git a/webapp/components/error_page.jsx b/webapp/components/error_page.jsx new file mode 100644 index 000000000..53f0fce82 --- /dev/null +++ b/webapp/components/error_page.jsx @@ -0,0 +1,58 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; + +import React from 'react'; +import {Link} from 'react-router'; + +import * as Utils from 'utils/utils.jsx'; + +export default class ErrorPage extends React.Component { + componentDidMount() { + $('body').attr('class', 'sticky error'); + } + componentWillUnmount() { + $('body').attr('class', ''); + } + render() { + let title = this.props.location.query.title; + if (!title || title === '') { + title = Utils.localizeMessage('error.generic.title', 'Error'); + } + + let message = this.props.location.query.message; + if (!message || message === '') { + message = Utils.localizeMessage('error.generic.message', 'An error has occoured.'); + } + + let link = this.props.location.query.link; + if (!link || link === '') { + link = '/'; + } + + let linkMessage = this.props.location.query.linkmessage; + if (!linkMessage || linkMessage === '') { + linkMessage = Utils.localizeMessage('error.generic.link_message', 'Back to Mattermost'); + } + + return ( + <div className='container-fluid'> + <div className='error__container'> + <div className='error__icon'> + <i className='fa fa-exclamation-triangle'/> + </div> + <h2>{title}</h2> + <p>{message}</p> + <Link to={link}>{linkMessage}</Link> + </div> + </div> + ); + } +} + +ErrorPage.defaultProps = { +}; +ErrorPage.propTypes = { + location: React.PropTypes.object +}; diff --git a/webapp/components/form_error.jsx b/webapp/components/form_error.jsx new file mode 100644 index 000000000..b7d1de16a --- /dev/null +++ b/webapp/components/form_error.jsx @@ -0,0 +1,50 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +export default class FormError extends React.Component { + static get propTypes() { + // accepts either a single error or an array of errors + return { + error: React.PropTypes.node, + errors: React.PropTypes.arrayOf(React.PropTypes.node) + }; + } + + static get defaultProps() { + return { + error: null, + errors: [] + }; + } + + render() { + if (!this.props.error && this.props.errors.length === 0) { + return null; + } + + // look for the first truthy error to display + let message = this.props.error; + + if (!message) { + for (const error of this.props.errors) { + if (error) { + message = error; + } + } + } + + if (!message) { + return null; + } + + return ( + <div className='form-group has-error'> + <label className='control-label'> + {message} + </label> + </div> + ); + } +} diff --git a/webapp/components/invite_member_modal.jsx b/webapp/components/invite_member_modal.jsx index 1f8fd6133..81c3a9629 100644 --- a/webapp/components/invite_member_modal.jsx +++ b/webapp/components/invite_member_modal.jsx @@ -50,6 +50,7 @@ class InviteMemberModal extends React.Component { constructor(props) { super(props); + this.teamChange = this.teamChange.bind(this); this.handleToggle = this.handleToggle.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.handleHide = this.handleHide.bind(this); @@ -68,16 +69,27 @@ class InviteMemberModal extends React.Component { emailEnabled: global.window.mm_config.SendEmailNotifications === 'true', userCreationEnabled: global.window.mm_config.EnableUserCreation === 'true', showConfirmModal: false, - isSendingEmails: false + isSendingEmails: false, + teamType: null }; } + teamChange() { + const team = TeamStore.getCurrent(); + const teamType = team ? team.type : null; + this.setState({ + teamType + }); + } + componentDidMount() { ModalStore.addModalListener(ActionTypes.TOGGLE_INVITE_MEMBER_MODAL, this.handleToggle); + TeamStore.addChangeListener(this.teamChange); } componentWillUnmount() { ModalStore.removeModalListener(ActionTypes.TOGGLE_INVITE_MEMBER_MODAL, this.handleToggle); + TeamStore.removeChangeListener(this.teamChange); } handleToggle(value) { @@ -224,7 +236,7 @@ class InviteMemberModal extends React.Component { var currentUser = UserStore.getCurrentUser(); const {formatMessage} = this.props.intl; - if (currentUser != null) { + if (currentUser != null && this.state.teamType != null) { var inviteSections = []; var inviteIds = this.state.inviteIds; for (var i = 0; i < inviteIds.length; i++) { @@ -398,7 +410,7 @@ class InviteMemberModal extends React.Component { ); } else if (this.state.userCreationEnabled) { var teamInviteLink = null; - if (currentUser && TeamStore.getCurrent().type === 'O') { + if (currentUser && this.state.teamType === 'O') { var link = ( <a href='#' diff --git a/webapp/components/logged_in.jsx b/webapp/components/logged_in.jsx index c6f7b50b1..0c4571083 100644 --- a/webapp/components/logged_in.jsx +++ b/webapp/components/logged_in.jsx @@ -10,13 +10,17 @@ import BrowserStore from 'stores/browser_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; +const TutorialSteps = Constants.TutorialSteps; +const Preferences = Constants.Preferences; import ErrorBar from 'components/error_bar.jsx'; import * as Websockets from 'action_creators/websocket_actions.jsx'; +import LoadingScreen from 'components/loading_screen.jsx'; import {browserHistory} from 'react-router'; import SidebarRight from 'components/sidebar_right.jsx'; import SidebarRightMenu from 'components/sidebar_right_menu.jsx'; +import Navbar from 'components/navbar.jsx'; // Modals import GetPostLinkModal from 'components/get_post_link_modal.jsx'; @@ -41,6 +45,14 @@ export default class LoggedIn extends React.Component { super(params); this.onUserChanged = this.onUserChanged.bind(this); + + this.state = { + user: null, + profiles: null + }; + } + isValidState() { + return this.state.user != null && this.state.profiles != null; } onUserChanged() { // Grab the current user @@ -66,6 +78,20 @@ export default class LoggedIn extends React.Component { Utils.applyTheme(Constants.THEMES.default); } } + + // Go to tutorial if we are first arrivign + const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999); + if (tutorialStep <= TutorialSteps.INTRO_SCREENS) { + browserHistory.push(Utils.getTeamURLFromAddressBar() + '/tutorial'); + } + + // Get profiles + const profiles = UserStore.getProfiles(); + + this.setState({ + user, + profiles + }); } componentWillMount() { // Emit view action @@ -177,6 +203,8 @@ export default class LoggedIn extends React.Component { Websockets.close(); UserStore.removeChangeListener(this.onUserChanged); + Utils.resetTheme(); + $('body').off('click.userpopover'); $('body').off('mouseenter mouseleave', '.post'); $('body').off('mouseenter mouseleave', '.post.post--comment.same--root'); @@ -186,14 +214,46 @@ export default class LoggedIn extends React.Component { $(window).off('keydown.preventBackspace'); } render() { + if (!this.isValidState()) { + return <LoadingScreen/>; + } + + let content = []; + if (this.props.children) { + content = this.props.children; + } else { + content.push( + this.props.navbar + ); + content.push( + this.props.sidebar + ); + content.push( + <div + key='inner-wrap' + className='inner-wrap channel__wrap' + > + <div className='row header'> + <div id='navbar'> + <Navbar/> + </div> + </div> + <div className='row main'> + {React.cloneElement(this.props.center, { + user: this.state.user, + profiles: this.state.profiles + })} + </div> + </div> + ); + } return ( <div className='channel-view'> <ErrorBar/> <div className='container-fluid'> <SidebarRight/> <SidebarRightMenu/> - {this.props.sidebar} - {this.props.center} + {content} <GetPostLinkModal/> <GetTeamInviteLinkModal/> @@ -216,8 +276,12 @@ LoggedIn.defaultProps = { }; LoggedIn.propTypes = { - children: React.PropTypes.object, - sidebar: React.PropTypes.object, - center: React.PropTypes.object, + children: React.PropTypes.oneOfType([ + React.PropTypes.arrayOf(React.PropTypes.element), + React.PropTypes.element + ]), + navbar: React.PropTypes.element, + sidebar: React.PropTypes.element, + center: React.PropTypes.element, params: React.PropTypes.object }; diff --git a/webapp/components/login_email.jsx b/webapp/components/login/components/login_email.jsx index d54c32ff9..b1f484c08 100644 --- a/webapp/components/login_email.jsx +++ b/webapp/components/login/components/login_email.jsx @@ -2,69 +2,40 @@ // See License.txt for license information. import * as Utils from 'utils/utils.jsx'; -import * as Client from 'utils/client.jsx'; import UserStore from 'stores/user_store.jsx'; -import {browserHistory} from 'react-router'; +import Constants from 'utils/constants.jsx'; -import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; - -var holders = defineMessages({ - badTeam: { - id: 'login_email.badTeam', - defaultMessage: 'Bad team name' - }, - emailReq: { - id: 'login_email.emailReq', - defaultMessage: 'An email is required' - }, - pwdReq: { - id: 'login_email.pwdReq', - defaultMessage: 'A password is required' - }, - email: { - id: 'login_email.email', - defaultMessage: 'Email' - }, - pwd: { - id: 'login_email.pwd', - defaultMessage: 'Password' - } -}); +import {FormattedMessage} from 'react-intl'; import React from 'react'; -class LoginEmail extends React.Component { +export default class LoginEmail extends React.Component { constructor(props) { super(props); this.handleSubmit = this.handleSubmit.bind(this); this.state = { - serverError: '' + serverError: props.serverError }; } + componentWillReceiveProps(nextProps) { + this.setState({serverError: nextProps.serverError}); + } handleSubmit(e) { e.preventDefault(); - const {formatMessage} = this.props.intl; var state = {}; - const name = this.props.teamName; - if (!name) { - state.serverError = formatMessage(holders.badTeam); - this.setState(state); - return; - } - const email = this.refs.email.value.trim(); if (!email) { - state.serverError = formatMessage(holders.emailReq); + state.serverError = Utils.localizeMessage('login_email.emailReq', 'An email is required'); this.setState(state); return; } const password = this.refs.password.value.trim(); if (!password) { - state.serverError = formatMessage(holders.pwdReq); + state.serverError = Utils.localizeMessage('login_email.pwdReq', 'A password is required'); this.setState(state); return; } @@ -72,21 +43,7 @@ class LoginEmail extends React.Component { state.serverError = ''; this.setState(state); - Client.loginByEmail(name, email, password, - () => { - UserStore.setLastEmail(email); - browserHistory.push('/' + name + '/channels/town-square'); - }, - (err) => { - if (err.id === 'api.user.login.not_verified.app_error') { - browserHistory.push('/verify_email?teamname=' + encodeURIComponent(name) + '&email=' + encodeURIComponent(email)); - return; - } - state.serverError = err.message; - this.valid = false; - this.setState(state); - } - ); + this.props.submit(Constants.EMAIL_SERVICE, email, password); } render() { let serverError; @@ -110,7 +67,6 @@ class LoginEmail extends React.Component { priorEmail = decodeURIComponent(emailParam); } - const {formatMessage} = this.props.intl; return ( <form onSubmit={this.handleSubmit}> <div className='signup__email-container'> @@ -125,7 +81,7 @@ class LoginEmail extends React.Component { name='email' defaultValue={priorEmail} ref='email' - placeholder={formatMessage(holders.email)} + placeholder={Utils.localizeMessage('login_email.email', 'Email')} spellCheck='false' /> </div> @@ -136,7 +92,7 @@ class LoginEmail extends React.Component { className='form-control' name='password' ref='password' - placeholder={formatMessage(holders.pwd)} + placeholder={Utils.localizeMessage('login_email.pwd', 'Password')} spellCheck='false' /> </div> @@ -160,8 +116,6 @@ LoginEmail.defaultProps = { }; LoginEmail.propTypes = { - intl: intlShape.isRequired, - teamName: React.PropTypes.string.isRequired + submit: React.PropTypes.func.isRequired, + serverError: React.PropTypes.string }; - -export default injectIntl(LoginEmail); diff --git a/webapp/components/login_ldap.jsx b/webapp/components/login/components/login_ldap.jsx index 59ff973dc..a2013710f 100644 --- a/webapp/components/login_ldap.jsx +++ b/webapp/components/login/components/login_ldap.jsx @@ -2,68 +2,39 @@ // See License.txt for license information. import * as Utils from 'utils/utils.jsx'; -import * as Client from 'utils/client.jsx'; +import Constants from 'utils/constants.jsx'; -import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; -import {browserHistory} from 'react-router'; - -const holders = defineMessages({ - badTeam: { - id: 'login_ldap.badTeam', - defaultMessage: 'Bad team name' - }, - idReq: { - id: 'login_ldap.idlReq', - defaultMessage: 'An LDAP ID is required' - }, - pwdReq: { - id: 'login_ldap.pwdReq', - defaultMessage: 'An LDAP password is required' - }, - username: { - id: 'login_ldap.username', - defaultMessage: 'LDAP Username' - }, - pwd: { - id: 'login_ldap.pwd', - defaultMessage: 'LDAP Password' - } -}); +import {FormattedMessage} from 'react-intl'; import React from 'react'; -class LoginLdap extends React.Component { +export default class LoginLdap extends React.Component { constructor(props) { super(props); this.handleSubmit = this.handleSubmit.bind(this); this.state = { - serverError: '' + serverError: props.serverError }; } + componentWillReceiveProps(nextProps) { + this.setState({serverError: nextProps.serverError}); + } handleSubmit(e) { e.preventDefault(); - const {formatMessage} = this.props.intl; - var state = {}; - - const teamName = this.props.teamName; - if (!teamName) { - state.serverError = formatMessage(holders.badTeam); - this.setState(state); - return; - } + const state = {}; const id = this.refs.id.value.trim(); if (!id) { - state.serverError = formatMessage(holders.idReq); + state.serverError = Utils.localizeMessage('login_ldap.idlReq', 'An LDAP ID is required'); this.setState(state); return; } const password = this.refs.password.value.trim(); if (!password) { - state.serverError = formatMessage(holders.pwdReq); + state.serverError = Utils.localizeMessage('login_ldap.pwdReq', 'An LDAP password is required'); this.setState(state); return; } @@ -71,20 +42,7 @@ class LoginLdap extends React.Component { state.serverError = ''; this.setState(state); - Client.loginByLdap(teamName, id, password, - () => { - const redirect = Utils.getUrlParameter('redirect'); - if (redirect) { - browserHistory.push(decodeURIComponent(redirect)); - } else { - browserHistory.push('/' + teamName + '/channels/town-square'); - } - }, - (err) => { - state.serverError = err.message; - this.setState(state); - } - ); + this.props.submit(Constants.LDAP_SERVICE, id, password); } render() { let serverError; @@ -93,7 +51,7 @@ class LoginLdap extends React.Component { serverError = <label className='control-label'>{this.state.serverError}</label>; errorClass = ' has-error'; } - const {formatMessage} = this.props.intl; + return ( <form onSubmit={this.handleSubmit}> <div className='signup__email-container'> @@ -105,7 +63,7 @@ class LoginLdap extends React.Component { autoFocus={true} className='form-control' ref='id' - placeholder={formatMessage(holders.username)} + placeholder={Utils.localizeMessage('login_ldap.username', 'LDAP Username')} spellCheck='false' /> </div> @@ -114,7 +72,7 @@ class LoginLdap extends React.Component { type='password' className='form-control' ref='password' - placeholder={formatMessage(holders.pwd)} + placeholder={Utils.localizeMessage('login_ldap.pwd', 'LDAP Password')} spellCheck='false' /> </div> @@ -138,8 +96,6 @@ LoginLdap.defaultProps = { }; LoginLdap.propTypes = { - intl: intlShape.isRequired, - teamName: React.PropTypes.string.isRequired + serverError: React.PropTypes.string, + submit: React.PropTypes.func.isRequired }; - -export default injectIntl(LoginLdap); diff --git a/webapp/components/login/components/login_mfa.jsx b/webapp/components/login/components/login_mfa.jsx new file mode 100644 index 000000000..f8ebf1e82 --- /dev/null +++ b/webapp/components/login/components/login_mfa.jsx @@ -0,0 +1,92 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Utils from 'utils/utils.jsx'; + +import {FormattedMessage} from 'react-intl'; + +import React from 'react'; + +export default class LoginMfa extends React.Component { + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + + this.state = { + serverError: '' + }; + } + handleSubmit(e) { + e.preventDefault(); + const state = {}; + + const token = this.refs.token.value.trim(); + if (!token) { + state.serverError = Utils.localizeMessage('login_mfa.tokenReq', 'Please enter an MFA token'); + this.setState(state); + return; + } + + state.serverError = ''; + this.setState(state); + + this.props.submit(this.props.method, this.props.loginId, this.props.password, token); + } + render() { + let serverError; + let errorClass = ''; + if (this.state.serverError) { + serverError = <label className='control-label'>{this.state.serverError}</label>; + errorClass = ' has-error'; + } + + return ( + <form onSubmit={this.handleSubmit}> + <div className='signup__email-container'> + <p> + <FormattedMessage + id='login_mfa.enterToken' + defaultMessage="To complete the sign in process, please enter a token from your smartphone's authenticator" + /> + </p> + <div className={'form-group' + errorClass}> + {serverError} + </div> + <div className={'form-group' + errorClass}> + <input + type='text' + className='form-control' + name='token' + ref='token' + placeholder={Utils.localizeMessage('login_mfa.token', 'MFA Token')} + spellCheck='false' + autoComplete='off' + autoFocus={true} + /> + </div> + <div className='form-group'> + <button + type='submit' + className='btn btn-primary' + > + <FormattedMessage + id='login_mfa.submit' + defaultMessage='Submit' + /> + </button> + </div> + </div> + </form> + ); + } +} +LoginMfa.defaultProps = { +}; + +LoginMfa.propTypes = { + method: React.PropTypes.string.isRequired, + loginId: React.PropTypes.string.isRequired, + password: React.PropTypes.string.isRequired, + submit: React.PropTypes.func.isRequired +}; diff --git a/webapp/components/login_username.jsx b/webapp/components/login/components/login_username.jsx index 71874fa1a..3cb213994 100644 --- a/webapp/components/login_username.jsx +++ b/webapp/components/login/components/login_username.jsx @@ -2,42 +2,10 @@ // See License.txt for license information. import * as Utils from 'utils/utils.jsx'; -import * as Client from 'utils/client.jsx'; import UserStore from 'stores/user_store.jsx'; +import Constants from 'utils/constants.jsx'; -import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; -import {browserHistory} from 'react-router'; - -var holders = defineMessages({ - badTeam: { - id: 'login_username.badTeam', - defaultMessage: 'Bad team name' - }, - usernameReq: { - id: 'login_username.usernameReq', - defaultMessage: 'A username is required' - }, - pwdReq: { - id: 'login_username.pwdReq', - defaultMessage: 'A password is required' - }, - verifyEmailError: { - id: 'login_username.verifyEmailError', - defaultMessage: 'Please verify your email address. Check your inbox for an email.' - }, - userNotFoundError: { - id: 'login_username.userNotFoundError', - defaultMessage: "We couldn't find an existing account matching your username for this team." - }, - username: { - id: 'login_username.username', - defaultMessage: 'Username' - }, - pwd: { - id: 'login_username.pwd', - defaultMessage: 'Password' - } -}); +import {FormattedMessage} from 'react-intl'; import React from 'react'; @@ -48,31 +16,26 @@ export default class LoginUsername extends React.Component { this.handleSubmit = this.handleSubmit.bind(this); this.state = { - serverError: '' + serverError: props.serverError }; } + componentWillReceiveProps(nextProps) { + this.setState({serverError: nextProps.serverError}); + } handleSubmit(e) { e.preventDefault(); - const {formatMessage} = this.props.intl; - var state = {}; - - const name = this.props.teamName; - if (!name) { - state.serverError = formatMessage(holders.badTeam); - this.setState(state); - return; - } + const state = {}; const username = this.refs.username.value.trim(); if (!username) { - state.serverError = formatMessage(holders.usernameReq); + state.serverError = Utils.localizeMessage('login_username.usernameReq', 'A username is required'); this.setState(state); return; } const password = this.refs.password.value.trim(); if (!password) { - state.serverError = formatMessage(holders.pwdReq); + state.serverError = Utils.localizeMessage('login_username.pwdReq', 'A password is required'); this.setState(state); return; } @@ -80,30 +43,7 @@ export default class LoginUsername extends React.Component { state.serverError = ''; this.setState(state); - Client.loginByUsername(name, username, password, - () => { - UserStore.setLastUsername(username); - - const redirect = Utils.getUrlParameter('redirect'); - if (redirect) { - browserHistory.push(decodeURIComponent(redirect)); - } else { - browserHistory.push('/' + name + '/channels/town-square'); - } - }, - (err) => { - if (err.id === 'api.user.login.not_verified.app_error') { - state.serverError = formatMessage(holders.verifyEmailError); - } else if (err.id === 'store.sql_user.get_by_username.app_error') { - state.serverError = formatMessage(holders.userNotFoundError); - } else { - state.serverError = err.message; - } - - this.valid = false; - this.setState(state); - } - ); + this.props.submit(Constants.USERNAME_SERVICE, username, password); } render() { let serverError; @@ -127,7 +67,6 @@ export default class LoginUsername extends React.Component { priorUsername = decodeURIComponent(emailParam); } - const {formatMessage} = this.props.intl; return ( <form onSubmit={this.handleSubmit}> <div className='signup__email-container'> @@ -142,7 +81,7 @@ export default class LoginUsername extends React.Component { name='username' defaultValue={priorUsername} ref='username' - placeholder={formatMessage(holders.username)} + placeholder={Utils.localizeMessage('login_username.username', 'Username')} spellCheck='false' /> </div> @@ -153,7 +92,7 @@ export default class LoginUsername extends React.Component { className='form-control' name='password' ref='password' - placeholder={formatMessage(holders.pwd)} + placeholder={Utils.localizeMessage('login_username.pwd', 'Password')} spellCheck='false' /> </div> @@ -177,8 +116,6 @@ LoginUsername.defaultProps = { }; LoginUsername.propTypes = { - intl: intlShape.isRequired, - teamName: React.PropTypes.string.isRequired + serverError: React.PropTypes.string, + submit: React.PropTypes.func.isRequired }; - -export default injectIntl(LoginUsername); diff --git a/webapp/components/login.jsx b/webapp/components/login/login.jsx index e8180895d..e867af47a 100644 --- a/webapp/components/login.jsx +++ b/webapp/components/login/login.jsx @@ -1,14 +1,17 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import LoginEmail from './login_email.jsx'; -import LoginUsername from './login_username.jsx'; -import LoginLdap from './login_ldap.jsx'; +import LoginEmail from './components/login_email.jsx'; +import LoginUsername from './components/login_username.jsx'; +import LoginLdap from './components/login_ldap.jsx'; +import LoginMfa from './components/login_mfa.jsx'; + +import TeamStore from 'stores/team_store.jsx'; +import UserStore from 'stores/user_store.jsx'; -import * as Utils from 'utils/utils.jsx'; import * as Client from 'utils/client.jsx'; +import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; -import TeamStore from 'stores/team_store.jsx'; import {FormattedMessage} from 'react-intl'; import {browserHistory, Link} from 'react-router'; @@ -21,8 +24,12 @@ export default class Login extends React.Component { this.getStateFromStores = this.getStateFromStores.bind(this); this.onTeamChange = this.onTeamChange.bind(this); + this.preSubmit = this.preSubmit.bind(this); + this.submit = this.submit.bind(this); - this.state = this.getStateFromStores(); + const state = this.getStateFromStores(); + state.doneCheckLogin = false; + this.state = state; } componentDidMount() { TeamStore.addChangeListener(this.onTeamChange); @@ -39,61 +46,95 @@ export default class Login extends React.Component { } getStateFromStores() { return { - currentTeam: TeamStore.getByName(this.props.params.team), - doneCheckLogin: false + currentTeam: TeamStore.getByName(this.props.params.team) }; } onTeamChange() { this.setState(this.getStateFromStores()); } - render() { - const currentTeam = this.state.currentTeam; - if (currentTeam == null || !this.state.doneCheckLogin) { - return <div/>; + preSubmit(method, loginId, password) { + if (global.window.mm_config.EnableMultifactorAuthentication !== 'true') { + this.submit(method, loginId, password, ''); + return; } - const teamDisplayName = currentTeam.display_name; - const teamName = currentTeam.name; - const ldapEnabled = global.window.mm_config.EnableLdap === 'true'; - const usernameSigninEnabled = global.window.mm_config.EnableSignInWithUsername === 'true'; + Client.checkMfa(method, this.state.currentTeam.name, loginId, + (data) => { + if (data.mfa_required === 'true') { + this.setState({showMfa: true, method, loginId, password}); + } else { + this.submit(method, loginId, password, ''); + } + }, + (err) => { + if (method === Constants.EMAIL_SERVICE) { + this.setState({serverEmailError: err.message}); + } else if (method === Constants.USERNAME_SERVICE) { + this.setState({serverUsernameError: err.message}); + } else if (method === Constants.LDAP_SERVICE) { + this.setState({serverLdapError: err.message}); + } + } + ); + } + submit(method, loginId, password, token) { + this.setState({showMfa: false, serverEmailError: null, serverUsernameError: null, serverLdapError: null}); - let loginMessage = []; - if (global.window.mm_config.EnableSignUpWithGitLab === 'true') { - loginMessage.push( - <Link - className='btn btn-custom-login gitlab' - key='gitlab' - to={'/api/v1/oauth/gitlab/login?team=' + encodeURIComponent(teamName)} - > - <span className='icon'/> - <span> - <FormattedMessage - id='login.gitlab' - defaultMessage='with GitLab' - /> - </span> - </Link> + const team = this.state.currentTeam.name; + + if (method === Constants.EMAIL_SERVICE) { + Client.loginByEmail(team, loginId, password, token, + () => { + UserStore.setLastEmail(loginId); + browserHistory.push('/' + team + '/channels/town-square'); + }, + (err) => { + if (err.id === 'api.user.login.not_verified.app_error') { + browserHistory.push('/verify_email?teamname=' + encodeURIComponent(name) + '&email=' + encodeURIComponent(loginId)); + return; + } + this.setState({serverEmailError: err.message}); + } ); - } + } else if (method === Constants.USERNAME_SERVICE) { + Client.loginByUsername(team, loginId, password, token, + () => { + UserStore.setLastUsername(loginId); - if (global.window.mm_config.EnableSignUpWithGoogle === 'true') { - loginMessage.push( - <Link - className='btn btn-custom-login google' - key='google' - to={'/api/v1/oauth/google/login?team=' + encodeURIComponent(teamName)} - > - <span className='icon'/> - <span> - <FormattedMessage - id='login.google' - defaultMessage='with Google Apps' - /> - </span> - </Link> + const redirect = Utils.getUrlParameter('redirect'); + if (redirect) { + browserHistory.push(decodeURIComponent(redirect)); + } else { + browserHistory.push('/' + team + '/channels/town-square'); + } + }, + (err) => { + if (err.id === 'api.user.login.not_verified.app_error') { + this.setState({serverUsernameError: Utils.localizeMessage('login_username.verifyEmailError', 'Please verify your email address. Check your inbox for an email.')}); + } else if (err.id === 'store.sql_user.get_by_username.app_error') { + this.setState({serverUsernameError: Utils.localizeMessage('login_username.userNotFoundError', 'We couldn\'t find an existing account matching your username for this team.')}); + } else { + this.setState({serverUsernameError: err.message}); + } + } + ); + } else if (method === Constants.LDAP_SERVICE) { + Client.loginByLdap(team, loginId, password, token, + () => { + const redirect = Utils.getUrlParameter('redirect'); + if (redirect) { + browserHistory.push(decodeURIComponent(redirect)); + } else { + browserHistory.push('/' + team + '/channels/town-square'); + } + }, + (err) => { + this.setState({serverLdapError: err.message}); + } ); } - + } + createLoginOptions(currentTeam) { const extraParam = Utils.getUrlParameter('extra'); let extraBox = ''; if (extraParam) { @@ -130,44 +171,126 @@ export default class Login extends React.Component { } } - let emailSignup; - if (global.window.mm_config.EnableSignInWithEmail === 'true') { - emailSignup = ( - <LoginEmail - teamName={teamName} - /> + const teamName = currentTeam.name; + const ldapEnabled = global.window.mm_config.EnableLdap === 'true'; + const gitlabSigninEnabled = global.window.mm_config.EnableSignUpWithGitLab === 'true'; + const googleSigninEnabled = global.window.mm_config.EnableSignUpWithGoogle === 'true'; + const usernameSigninEnabled = global.window.mm_config.EnableSignInWithUsername === 'true'; + const emailSigninEnabled = global.window.mm_config.EnableSignInWithEmail === 'true'; + + const oauthLogins = []; + if (gitlabSigninEnabled) { + oauthLogins.push( + <Link + className='btn btn-custom-login gitlab' + key='gitlab' + to={'/api/v1/oauth/gitlab/login?team=' + encodeURIComponent(teamName)} + > + <span className='icon'/> + <span> + <FormattedMessage + id='login.gitlab' + defaultMessage='with GitLab' + /> + </span> + </Link> ); } - if (loginMessage.length > 0 && emailSignup) { - loginMessage = ( - <div> - {loginMessage} - <div className='or__container'> + if (googleSigninEnabled) { + oauthLogins.push( + <Link + className='btn btn-custom-login google' + key='google' + to={'/api/v1/oauth/google/login?team=' + encodeURIComponent(teamName)} + > + <span className='icon'/> + <span> <FormattedMessage - id='login.or' - defaultMessage='or' + id='login.google' + defaultMessage='with Google Apps' /> + </span> + </Link> + ); + } + + let emailLogin; + if (emailSigninEnabled) { + emailLogin = ( + <LoginEmail + teamName={teamName} + serverError={this.state.serverEmailError} + submit={this.preSubmit} + /> + ); + + if (oauthLogins.length > 0) { + emailLogin = ( + <div> + <div className='or__container'> + <FormattedMessage + id='login.or' + defaultMessage='or' + /> + </div> + {emailLogin} </div> - </div> + ); + } + } + + let usernameLogin; + if (usernameSigninEnabled) { + usernameLogin = ( + <LoginUsername + teamName={teamName} + serverError={this.state.serverUsernameError} + submit={this.preSubmit} + /> ); + + if (emailSigninEnabled || oauthLogins.length > 0) { + usernameLogin = ( + <div> + <div className='or__container'> + <FormattedMessage + id='login.or' + defaultMessage='or' + /> + </div> + {usernameLogin} + </div> + ); + } } - let forgotPassword; - if (emailSignup) { - forgotPassword = ( - <div className='form-group'> - <Link to={'/' + teamName + '/reset_password'}> - <FormattedMessage - id='login.forgot' - defaultMessage='I forgot my password' - /> - </Link> - </div> + let ldapLogin; + if (ldapEnabled) { + ldapLogin = ( + <LoginLdap + teamName={teamName} + serverError={this.state.serverLdapError} + submit={this.preSubmit} + /> ); + + if (emailSigninEnabled || usernameSigninEnabled || oauthLogins.length > 0) { + ldapLogin = ( + <div> + <div className='or__container'> + <FormattedMessage + id='login.or' + defaultMessage='or' + /> + </div> + {ldapLogin} + </div> + ); + } } - let userSignUp = null; + let userSignUp; if (currentTeam.allow_open_invite) { userSignUp = ( <div> @@ -190,7 +313,21 @@ export default class Login extends React.Component { ); } - let teamSignUp = null; + let forgotPassword; + if (usernameSigninEnabled || emailSigninEnabled) { + forgotPassword = ( + <div className='form-group'> + <Link to={'/' + teamName + '/reset_password'}> + <FormattedMessage + id='login.forgot' + defaultMessage='I forgot my password' + /> + </Link> + </div> + ); + } + + let teamSignUp; if (global.window.mm_config.EnableTeamCreation === 'true' && !Utils.isMobileApp()) { teamSignUp = ( <div className='margin--extra'> @@ -207,54 +344,37 @@ export default class Login extends React.Component { ); } - let ldapLogin = null; - if (global.window.mm_config.EnableLdap === 'true') { - ldapLogin = ( - <LoginLdap - teamName={teamName} - /> - ); - } - - if (ldapEnabled && (loginMessage.length > 0 || emailSignup || usernameSigninEnabled)) { - ldapLogin = ( - <div> - <div className='or__container'> - <FormattedMessage - id='login.or' - defaultMessage='or' - /> - </div> - <LoginLdap - teamName={teamName} - /> - </div> - ); + return ( + <div> + {extraBox} + {oauthLogins} + {emailLogin} + {usernameLogin} + {ldapLogin} + {userSignUp} + {forgotPassword} + {teamSignUp} + </div> + ); + } + render() { + const currentTeam = this.state.currentTeam; + if (currentTeam == null || !this.state.doneCheckLogin) { + return <div/>; } - let usernameLogin = null; - if (global.window.mm_config.EnableSignInWithUsername === 'true') { - usernameLogin = ( - <LoginUsername - teamName={teamName} + let content; + if (this.state.showMfa) { + content = ( + <LoginMfa + method={this.state.method} + loginId={this.state.loginId} + password={this.state.password} + submit={this.submit} /> ); - } - - if (usernameSigninEnabled && (loginMessage.length > 0 || emailSignup || ldapEnabled)) { - usernameLogin = ( - <div> - <div className='or__container'> - <FormattedMessage - id='login.or' - defaultMessage='or' - /> - </div> - <LoginUsername - teamName={teamName} - /> - </div> - ); + } else { + content = this.createLoginOptions(currentTeam); } return ( @@ -275,7 +395,7 @@ export default class Login extends React.Component { defaultMessage='Sign in to:' /> </h5> - <h2 className='signup-team__name'>{teamDisplayName}</h2> + <h2 className='signup-team__name'>{currentTeam.display_name}</h2> <h2 className='signup-team__subdomain'> <FormattedMessage id='login.on' @@ -285,14 +405,7 @@ export default class Login extends React.Component { }} /> </h2> - {extraBox} - {loginMessage} - {emailSignup} - {usernameLogin} - {ldapLogin} - {userSignUp} - {forgotPassword} - {teamSignUp} + {content} </div> </div> </div> diff --git a/webapp/components/more_channels.jsx b/webapp/components/more_channels.jsx index d0eeec1ef..811bb8101 100644 --- a/webapp/components/more_channels.jsx +++ b/webapp/components/more_channels.jsx @@ -9,6 +9,7 @@ import * as AsyncClient from 'utils/async_client.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import LoadingScreen from './loading_screen.jsx'; import NewChannelFlow from './new_channel_flow.jsx'; +import * as GlobalActions from 'action_creators/global_actions.jsx'; import {FormattedMessage} from 'react-intl'; @@ -64,8 +65,7 @@ export default class MoreChannels extends React.Component { client.joinChannel(channel.id, () => { $(ReactDOM.findDOMNode(this.refs.modal)).modal('hide'); - AsyncClient.getChannel(channel.id); - Utils.switchChannel(channel); + GlobalActions.emitChannelClickEvent(channel); this.setState({joiningChannel: -1}); }, (err) => { diff --git a/webapp/components/more_direct_channels.jsx b/webapp/components/more_direct_channels.jsx index d1446059d..29d64517e 100644 --- a/webapp/components/more_direct_channels.jsx +++ b/webapp/components/more_direct_channels.jsx @@ -5,6 +5,7 @@ import {Modal} from 'react-bootstrap'; import FilteredUserList from './filtered_user_list.jsx'; import UserStore from 'stores/user_store.jsx'; import * as Utils from 'utils/utils.jsx'; +import * as GlobalActions from 'action_creators/global_actions.jsx'; import {FormattedMessage} from 'react-intl'; import SpinnerButton from 'components/spinner_button.jsx'; @@ -68,7 +69,7 @@ export default class MoreDirectChannels extends React.Component { Utils.openDirectChannelToUser( teammate, (channel) => { - Utils.switchChannel(channel); + GlobalActions.emitChannelClickEvent(channel); this.setState({loadingDMChannel: -1}); this.handleHide(); }, @@ -85,6 +86,7 @@ export default class MoreDirectChannels extends React.Component { createJoinDirectChannelButton({user}) { return ( <SpinnerButton + className='btn btm-sm btn-primary' spinning={this.state.loadingDMChannel === user.id} onClick={this.handleShowDirectChannel.bind(this, user)} > diff --git a/webapp/components/msg_typing.jsx b/webapp/components/msg_typing.jsx index b2d414287..631eea78d 100644 --- a/webapp/components/msg_typing.jsx +++ b/webapp/components/msg_typing.jsx @@ -40,13 +40,15 @@ class MsgTyping extends React.Component { } updateTypingText(typingUsers) { - if (!typingUsers) { - return; + let text = ''; + let users = {}; + let numUsers = 0; + if (typingUsers) { + users = Object.keys(typingUsers); + numUsers = users.length; } - const users = Object.keys(typingUsers); - let text = ''; - switch (users.length) { + switch (numUsers) { case 0: text = ''; break; diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx index e58e142d0..5afd7e683 100644 --- a/webapp/components/navbar.jsx +++ b/webapp/components/navbar.jsx @@ -45,6 +45,7 @@ export default class Navbar extends React.Component { this.showEditChannelHeaderModal = this.showEditChannelHeaderModal.bind(this); this.showRenameChannelModal = this.showRenameChannelModal.bind(this); this.hideRenameChannelModal = this.hideRenameChannelModal.bind(this); + this.isStateValid = this.isStateValid.bind(this); this.createCollapseButtons = this.createCollapseButtons.bind(this); this.createDropdown = this.createDropdown.bind(this); @@ -64,7 +65,7 @@ export default class Navbar extends React.Component { currentUser: UserStore.getCurrentUser() }; } - stateValid() { + isStateValid() { return this.state.channel && this.state.member && this.state.users && this.state.currentUser; } componentDidMount() { @@ -422,7 +423,7 @@ export default class Navbar extends React.Component { return buttons; } render() { - if (!this.stateValid()) { + if (!this.isStateValid()) { return null; } diff --git a/webapp/components/navbar_dropdown.jsx b/webapp/components/navbar_dropdown.jsx index 7e42a71ea..da1ae237e 100644 --- a/webapp/components/navbar_dropdown.jsx +++ b/webapp/components/navbar_dropdown.jsx @@ -59,6 +59,7 @@ export default class NavbarDropdown extends React.Component { var isAdmin = false; var isSystemAdmin = false; var teamSettings = null; + let integrationsLink = null; if (currentUser != null) { isAdmin = Utils.isAdmin(currentUser.roles); @@ -125,6 +126,21 @@ export default class NavbarDropdown extends React.Component { ); } + if (window.mm_config.EnableIncomingWebhooks === 'true' || window.mm_config.EnableOutgoingWebhooks === 'true') { + if (isAdmin || window.EnableAdminOnlyIntegrations !== 'true') { + integrationsLink = ( + <li> + <Link to={'/settings/integrations'}> + <FormattedMessage + id='navbar_dropdown.integrations' + defaultMessage='Integrations' + /> + </Link> + </li> + ); + } + } + if (isSystemAdmin) { sysAdminLink = ( <li> @@ -238,6 +254,7 @@ export default class NavbarDropdown extends React.Component { </li> {adminDivider} {teamSettings} + {integrationsLink} {manageLink} {sysAdminLink} {teams} diff --git a/webapp/components/new_channel_flow.jsx b/webapp/components/new_channel_flow.jsx index 30035ee5d..8c66ef3ce 100644 --- a/webapp/components/new_channel_flow.jsx +++ b/webapp/components/new_channel_flow.jsx @@ -2,9 +2,9 @@ // See License.txt for license information. import * as Utils from 'utils/utils.jsx'; -import * as AsyncClient from 'utils/async_client.jsx'; import * as Client from 'utils/client.jsx'; import UserStore from 'stores/user_store.jsx'; +import * as GlobalActions from 'action_creators/global_actions.jsx'; import NewChannelModal from './new_channel_modal.jsx'; import ChangeURLModal from './change_url_modal.jsx'; @@ -110,8 +110,7 @@ class NewChannelFlow extends React.Component { Client.createChannel(channel, (data) => { this.props.onModalDismissed(); - AsyncClient.getChannel(data.id); - Utils.switchChannel(data); + GlobalActions.emitChannelClickEvent(data); }, (err) => { if (err.id === 'model.channel.is_valid.2_or_more.app_error') { @@ -247,4 +246,4 @@ NewChannelFlow.propTypes = { onModalDismissed: React.PropTypes.func.isRequired }; -export default injectIntl(NewChannelFlow);
\ No newline at end of file +export default injectIntl(NewChannelFlow); diff --git a/webapp/components/permalink_view.jsx b/webapp/components/permalink_view.jsx new file mode 100644 index 000000000..2c32d643d --- /dev/null +++ b/webapp/components/permalink_view.jsx @@ -0,0 +1,93 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import ChannelHeader from 'components/channel_header.jsx'; +import PostFocusView from 'components/post_focus_view.jsx'; + +import ChannelStore from 'stores/channel_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; + +import {Link} from 'react-router'; +import {FormattedMessage} from 'react-intl'; + +export default class PermalinkView extends React.Component { + constructor(props) { + super(props); + + this.getStateFromStores = this.getStateFromStores.bind(this); + this.isStateValid = this.isStateValid.bind(this); + this.updateState = this.updateState.bind(this); + + this.state = this.getStateFromStores(props); + } + getStateFromStores(props) { + const postId = props.params.postid; + const channel = ChannelStore.getCurrent(); + const channelId = channel ? channel.id : ''; + const channelName = channel ? channel.name : ''; + const team = TeamStore.getCurrent(); + const teamName = team ? team.name : ''; + return { + channelId, + channelName, + teamName, + postId + }; + } + isStateValid() { + return this.state.channelId !== '' && this.state.teamName; + } + updateState() { + this.setState(this.getStateFromStores(this.props)); + } + componentDidMount() { + ChannelStore.addChangeListener(this.updateState); + TeamStore.addChangeListener(this.updateState); + } + componentWillUnmount() { + ChannelStore.removeChangeListener(this.updateState); + TeamStore.removeChangeListener(this.updateState); + } + componentWillReceiveProps(nextProps) { + this.setState(this.getStateFromStores(nextProps)); + } + render() { + if (!this.isStateValid()) { + return null; + } + return ( + <div + id='app-content' + className='app__content' + > + <ChannelHeader + channelId={this.state.channelId} + /> + <PostFocusView profiles={this.props.profiles}/> + <div + id='archive-link-home' + > + <Link + to={'/' + this.state.teamName + '/channels/' + this.state.channelName} + > + <FormattedMessage + id='center_panel.recent' + defaultMessage='Click here to jump to recent messages. ' + /> + <i className='fa fa-arrow-down'></i> + </Link> + </div> + </div> + ); + } +} + +PermalinkView.defaultProps = { +}; + +PermalinkView.propTypes = { + params: React.PropTypes.object.isRequired, + profiles: React.PropTypes.object +}; diff --git a/webapp/components/popover_list_members.jsx b/webapp/components/popover_list_members.jsx index 819c7f590..226a1889c 100644 --- a/webapp/components/popover_list_members.jsx +++ b/webapp/components/popover_list_members.jsx @@ -6,10 +6,9 @@ import $ from 'jquery'; import UserStore from 'stores/user_store.jsx'; import {Popover, Overlay} from 'react-bootstrap'; import * as Utils from 'utils/utils.jsx'; +import * as GlobalActions from 'action_creators/global_actions.jsx'; import Constants from 'utils/constants.jsx'; -import ChannelStore from 'stores/channel_store.jsx'; - import {FormattedMessage} from 'react-intl'; import React from 'react'; @@ -36,7 +35,7 @@ export default class PopoverListMembers extends React.Component { Utils.openDirectChannelToUser( teammate, (channel, channelAlreadyExisted) => { - Utils.switchChannel(channel); + GlobalActions.emitChannelClickEvent(channel); if (channelAlreadyExisted) { this.closePopover(); } @@ -56,7 +55,6 @@ export default class PopoverListMembers extends React.Component { const members = this.props.members; const teamMembers = UserStore.getProfilesUsernameMap(); const currentUserId = UserStore.getCurrentId(); - const ch = ChannelStore.getCurrent(); if (members && teamMembers) { members.sort((a, b) => { @@ -68,7 +66,7 @@ export default class PopoverListMembers extends React.Component { members.forEach((m, i) => { let button = ''; - if (currentUserId !== m.id && ch.type !== 'D') { + if (currentUserId !== m.id && this.props.channel.type !== 'D') { button = ( <a href='#' @@ -176,7 +174,7 @@ export default class PopoverListMembers extends React.Component { } PopoverListMembers.propTypes = { + channel: React.PropTypes.object.isRequired, members: React.PropTypes.array.isRequired, - memberCount: React.PropTypes.number, - channelId: React.PropTypes.string.isRequired + memberCount: React.PropTypes.number }; diff --git a/webapp/components/post.jsx b/webapp/components/post.jsx index f2818188a..bbf8d9bf6 100644 --- a/webapp/components/post.jsx +++ b/webapp/components/post.jsx @@ -129,6 +129,7 @@ export default class Post extends React.Component { const post = this.props.post; const parentPost = this.props.parentPost; const posts = this.props.posts; + const mattermostLogo = Constants.MATTERMOST_ICON_SVG; if (!post.props) { post.props = {}; @@ -188,9 +189,9 @@ export default class Post extends React.Component { if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') { if (post.props.override_icon_url) { src = post.props.override_icon_url; + } else { + src = Constants.DEFAULT_WEBHOOK_LOGO; } - } else if (Utils.isSystemMessage(post)) { - src = Constants.SYSTEM_MESSAGE_PROFILE_IMAGE; } profilePic = ( @@ -200,6 +201,15 @@ export default class Post extends React.Component { width='36' /> ); + + if (Utils.isSystemMessage(post)) { + profilePic = ( + <span + className='icon' + dangerouslySetInnerHTML={{__html: mattermostLogo}} + /> + ); + } } return ( diff --git a/webapp/components/post_body_additional_content.jsx b/webapp/components/post_body_additional_content.jsx index 2cd82f213..452597dde 100644 --- a/webapp/components/post_body_additional_content.jsx +++ b/webapp/components/post_body_additional_content.jsx @@ -70,7 +70,7 @@ export default class PostBodyAdditionalContent extends React.Component { return this.getSlackAttachment(); } - const link = Utils.extractLinks(this.props.post.message)[0]; + const link = Utils.extractFirstLink(this.props.post.message); if (!link) { return null; } diff --git a/webapp/components/post_info.jsx b/webapp/components/post_info.jsx index 0aa71edd7..2d41b0e54 100644 --- a/webapp/components/post_info.jsx +++ b/webapp/components/post_info.jsx @@ -22,7 +22,7 @@ export default class PostInfo extends React.Component { } dropdownPosition(e) { var position = $('#post-list').height() - $(e.target).offset().top; - var dropdown = $(e.target).next('.dropdown-menu'); + var dropdown = $(e.target).closest('.col__reply').find('.dropdown-menu'); if (position < dropdown.height()) { dropdown.addClass('bottom'); } diff --git a/webapp/components/posts_view.jsx b/webapp/components/posts_view.jsx index 8b4b0c662..ffe04daa1 100644 --- a/webapp/components/posts_view.jsx +++ b/webapp/components/posts_view.jsx @@ -173,24 +173,15 @@ export default class PostsView extends React.Component { const postFromWebhook = Boolean(post.props && post.props.from_webhook); const prevPostFromWebhook = Boolean(prevPost.props && prevPost.props.from_webhook); const prevPostUserId = Utils.isSystemMessage(prevPost) ? '' : prevPost.user_id; - let prevWebhookName = ''; - if (prevPost.props && prevPost.props.override_username) { - prevWebhookName = prevPost.props.override_username; - } - let curWebhookName = ''; - if (post.props && post.props.override_username) { - curWebhookName = post.props.override_username; - } // consider posts from the same user if: // the previous post was made by the same user as the current post, // the previous post was made within 5 minutes of the current post, - // the previous post and current post are both from webhooks or both not, - // the previous post and current post have the same webhook usernames + // the current post is not from a webhook + // the previous post is not from a webhook if (prevPostUserId === postUserId && post.create_at - prevPost.create_at <= 1000 * 60 * 5 && - postFromWebhook === prevPostFromWebhook && - prevWebhookName === curWebhookName) { + !postFromWebhook && !prevPostFromWebhook) { sameUser = true; } @@ -213,13 +204,11 @@ export default class PostsView extends React.Component { // the previous post was made by the same user as the current post, // the previous post is not a comment, // the current post is not a comment, - // the previous post and current post are both from webhooks or both not, - // the previous post and current post have the same webhook usernames + // the current post is not from a webhook if (prevPostUserId === postUserId && !prevPostIsComment && !postIsComment && - postFromWebhook === prevPostFromWebhook && - prevWebhookName === curWebhookName) { + !postFromWebhook) { hideProfilePic = true; } } @@ -319,7 +308,7 @@ export default class PostsView extends React.Component { if (this.props.scrollType === PostsView.SCROLL_TYPE_BOTTOM) { this.scrollToBottom(); } else if (this.props.scrollType === PostsView.SCROLL_TYPE_NEW_MESSAGE) { - window.requestAnimationFrame(() => { + window.setTimeout(window.requestAnimationFrame(() => { // If separator exists scroll to it. Otherwise scroll to bottom. if (this.refs.newMessageSeparator) { var objDiv = this.refs.postlist; @@ -327,7 +316,7 @@ export default class PostsView extends React.Component { } else if (this.refs.postlist) { this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight; } - }); + }), 0); } else if (this.props.scrollType === PostsView.SCROLL_TYPE_POST && this.props.scrollPostId) { window.requestAnimationFrame(() => { const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPostId]); @@ -384,6 +373,8 @@ export default class PostsView extends React.Component { } componentWillUnmount() { window.removeEventListener('resize', this.handleResize); + this.scrollStopAction.cancel(); + PreferenceStore.removeChangeListener(this.updateState); } componentDidUpdate() { if (this.props.postList != null) { @@ -570,7 +561,9 @@ function FloatingTimestamp({isScrolling, post}) { return ( <div className={className}> - <span>{dateString}</span> + <div> + <span>{dateString}</span> + </div> </div> ); } diff --git a/webapp/components/posts_view_container.jsx b/webapp/components/posts_view_container.jsx index 7e334d4b0..a49c77f8d 100644 --- a/webapp/components/posts_view_container.jsx +++ b/webapp/components/posts_view_container.jsx @@ -8,7 +8,6 @@ import ChannelStore from 'stores/channel_store.jsx'; import PostStore from 'stores/post_store.jsx'; import UserStore from 'stores/user_store.jsx'; -import * as Utils from 'utils/utils.jsx'; import * as GlobalActions from 'action_creators/global_actions.jsx'; import Constants from 'utils/constants.jsx'; @@ -158,17 +157,6 @@ export default class PostsViewContainer extends React.Component { this.setState({scrollType: PostsView.SCROLL_TYPE_FREE}); } } - shouldComponentUpdate(nextProps, nextState) { - if (!Utils.areObjectsEqual(this.state, nextState)) { - return true; - } - - if (!Utils.areObjectsEqual(this.props, nextProps)) { - return true; - } - - return false; - } render() { const postLists = this.state.postLists; const channels = this.state.channels; diff --git a/webapp/components/removed_from_channel_modal.jsx b/webapp/components/removed_from_channel_modal.jsx index cdd51bd6e..45018ac99 100644 --- a/webapp/components/removed_from_channel_modal.jsx +++ b/webapp/components/removed_from_channel_modal.jsx @@ -6,7 +6,7 @@ import ReactDOM from 'react-dom'; import ChannelStore from 'stores/channel_store.jsx'; import UserStore from 'stores/user_store.jsx'; import BrowserStore from 'stores/browser_store.jsx'; -import * as utils from 'utils/utils.jsx'; +import * as GlobalActions from 'action_creators/global_actions.jsx'; import {FormattedMessage} from 'react-intl'; @@ -33,7 +33,7 @@ export default class RemovedFromChannelModal extends React.Component { } var townSquare = ChannelStore.getByName('town-square'); - setTimeout(() => utils.switchChannel(townSquare), 1); + setTimeout(() => GlobalActions.emitChannelClickEvent(townSquare), 1); this.setState(newState); } diff --git a/webapp/components/rename_channel_modal.jsx b/webapp/components/rename_channel_modal.jsx index 72828984c..ced3c2d2b 100644 --- a/webapp/components/rename_channel_modal.jsx +++ b/webapp/components/rename_channel_modal.jsx @@ -4,7 +4,7 @@ import ReactDOM from 'react-dom'; import * as Utils from 'utils/utils.jsx'; import * as Client from 'utils/client.jsx'; -import * as AsyncClient from 'utils/async_client.jsx'; +import * as GlobalActions from 'action_creators/global_actions.jsx'; import Constants from 'utils/constants.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; @@ -165,8 +165,7 @@ export default class RenameChannelModal extends React.Component { Client.updateChannel( channel, () => { - AsyncClient.getChannel(channel.id); - Utils.updateAddressBar(channel.name); + GlobalActions.emitChannelClickEvent(channel); this.handleHide(); }, diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx index 26b392aa1..7a7c5f692 100644 --- a/webapp/components/rhs_root_post.jsx +++ b/webapp/components/rhs_root_post.jsx @@ -217,6 +217,8 @@ export default class RhsRootPost extends React.Component { if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') { if (post.props.override_icon_url) { src = post.props.override_icon_url; + } else { + src = Constants.DEFAULT_WEBHOOK_LOGO; } } else if (Utils.isSystemMessage(post)) { src = Constants.SYSTEM_MESSAGE_PROFILE_IMAGE; diff --git a/webapp/components/root.jsx b/webapp/components/root.jsx index 9963bc5dd..3b85b23fd 100644 --- a/webapp/components/root.jsx +++ b/webapp/components/root.jsx @@ -69,7 +69,7 @@ export default class Root extends React.Component { FastClick.attach(document.body); // Get our localizaiton - GlobalActions.newLocalizationSelected('en'); + GlobalActions.loadBrowserLocale(); } componentWillUnmount() { LocalizationStore.removeChangeListener(this.localizationChanged); diff --git a/webapp/components/search_results_item.jsx b/webapp/components/search_results_item.jsx index 35769d06b..75cbcb2a0 100644 --- a/webapp/components/search_results_item.jsx +++ b/webapp/components/search_results_item.jsx @@ -1,7 +1,6 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import $ from 'jquery'; import UserStore from 'stores/user_store.jsx'; import UserProfile from './user_profile.jsx'; import * as GlobalActions from 'action_creators/global_actions.jsx'; @@ -10,28 +9,16 @@ import * as TextFormatting from 'utils/text_formatting.jsx'; import Constants from 'utils/constants.jsx'; import {FormattedMessage, FormattedDate} from 'react-intl'; - import React from 'react'; +import {Link} from 'react-router'; export default class SearchResultsItem extends React.Component { constructor(props) { super(props); - this.handleClick = this.handleClick.bind(this); this.handleFocusRHSClick = this.handleFocusRHSClick.bind(this); } - handleClick(e) { - e.preventDefault(); - - GlobalActions.emitPostFocusEvent(this.props.post.id); - - if ($(window).width() < 768) { - $('.sidebar--right').removeClass('move--left'); - $('.inner-wrap').removeClass('move--left'); - } - } - handleFocusRHSClick(e) { e.preventDefault(); GlobalActions.emitPostFocusRightHandSideFromSearch(this.props.post, this.props.isMentionSearch); @@ -99,16 +86,15 @@ export default class SearchResultsItem extends React.Component { </time> </li> <li> - <a - href='#' + <Link + to={'/' + window.location.pathname.split('/')[1] + '/pl/' + this.props.post.id} className='search-item__jump' - onClick={this.handleClick} > <FormattedMessage id='search_item.jump' defaultMessage='Jump' /> - </a> + </Link> </li> <li> <a diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx index c0d4755ed..500e73cf2 100644 --- a/webapp/components/sidebar.jsx +++ b/webapp/components/sidebar.jsx @@ -15,7 +15,6 @@ import TeamStore from 'stores/team_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; -import * as Client from 'utils/client.jsx'; import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; @@ -29,7 +28,7 @@ import {Tooltip, OverlayTrigger} from 'react-bootstrap'; import loadingGif from 'images/load.gif'; import React from 'react'; -import {browserHistory} from 'react-router'; +import {browserHistory, Link} from 'react-router'; import favicon from 'images/favicon/favicon-16x16.png'; import redFavicon from 'images/favicon/redfavicon-16x16.png'; @@ -139,7 +138,9 @@ export default class Sidebar extends React.Component { unreadCounts: JSON.parse(JSON.stringify(ChannelStore.getUnreadCounts())), showTutorialTip: tutorialStep === TutorialSteps.CHANNEL_POPOVER, currentTeam: TeamStore.getCurrent(), - currentUser: UserStore.getCurrentUser() + currentUser: UserStore.getCurrentUser(), + townSquare: ChannelStore.getByName(Constants.DEFAULT_CHANNEL), + offTopic: ChannelStore.getByName(Constants.OFFTOPIC_CHANNEL) }; } @@ -239,7 +240,9 @@ export default class Sidebar extends React.Component { }); } - handleLeaveDirectChannel(channel) { + handleLeaveDirectChannel(e, channel) { + e.preventDefault(); + if (!this.isLeaving.get(channel.id)) { this.isLeaving.set(channel.id, true); @@ -259,7 +262,7 @@ export default class Sidebar extends React.Component { } if (channel.id === this.state.activeId) { - Utils.switchChannel(ChannelStore.getByName(Constants.DEFAULT_CHANNEL)); + browserHistory.push('/' + this.state.currentTeam.name + '/channels/town-square'); } } @@ -289,6 +292,16 @@ export default class Sidebar extends React.Component { createTutorialTip() { const screens = []; + let townSquareDisplayName = Constants.DEFAULT_CHANNEL_UI_NAME; + if (this.state.townSquare) { + townSquareDisplayName = this.state.townSquare.display_name; + } + + let offTopicDisplayName = Constants.OFFTOPIC_CHANNEL_UI_NAME; + if (this.state.offTopic) { + offTopicDisplayName = this.state.offTopic.display_name; + } + screens.push( <div> <FormattedHTMLMessage @@ -302,10 +315,14 @@ export default class Sidebar extends React.Component { <div> <FormattedHTMLMessage id='sidebar.tutorialScreen2' - defaultMessage='<h4>"Town Square" and "Off-Topic" channels</h4> + defaultMessage='<h4>"{townsquare}" and "{offtopic}" channels</h4> <p>Here are two public channels to start:</p> - <p><strong>Town Square</strong> is a place for team-wide communication. Everyone in your team is a member of this channel.</p> - <p><strong>Off-Topic</strong> is a place for fun and humor outside of work-related channels. You and your team can decide what other channels to create.</p>' + <p><strong>{townsquare}</strong> is a place for team-wide communication. Everyone in your team is a member of this channel.</p> + <p><strong>{offtopic}</strong> is a place for fun and humor outside of work-related channels. You and your team can decide what other channels to create.</p>' + values={{ + townsquare: townSquareDisplayName, + offtopic: offTopicDisplayName + }} /> </div> ); @@ -406,48 +423,6 @@ export default class Sidebar extends React.Component { icon = <div className='status'><i className='fa fa-lock'></i></div>; } - // set up click handler to switch channels (or create a new channel for non-existant ones) - var handleClick = null; - - if (!channel.fake) { - handleClick = function clickHandler(e) { - if (e.target.attributes.getNamedItem('data-close')) { - handleClose(channel); - } else { - Utils.switchChannel(channel); - } - - e.preventDefault(); - }; - } else if (channel.fake) { - // 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(); - - if (e.target.attributes.getNamedItem('data-close')) { - handleClose(channel); - } else { - this.setState({loadingDMChannel: index}); - - Client.createDirectChannel(channel, otherUserId, - (data) => { - this.setState({loadingDMChannel: -1}); - AsyncClient.getChannel(data.id); - Utils.switchChannel(data); - }, - () => { - this.setState({loadingDMChannel: -1}); - browserHistory('/' + this.state.currentTeam.name); - } - ); - } - }.bind(this); - } - } - let closeButton = null; const removeTooltip = ( <Tooltip id='remove-dm-tooltip'> @@ -464,12 +439,12 @@ export default class Sidebar extends React.Component { placement='top' overlay={removeTooltip} > - <span - className='btn-close' - data-close='true' - > - {'Γ'} - </span> + <span + onClick={(e) => handleClose(e, channel)} + className='btn-close' + > + {'Γ'} + </span> </OverlayTrigger> ); @@ -481,23 +456,29 @@ export default class Sidebar extends React.Component { tutorialTip = this.createTutorialTip(); } + let link = ''; + if (channel.fake) { + link = '/' + this.state.currentTeam.name + '/channels/' + channel.name + '?fakechannel=' + encodeURIComponent(JSON.stringify(channel)); + } else { + link = '/' + this.state.currentTeam.name + '/channels/' + channel.name; + } + return ( <li key={channel.name} ref={channel.name} className={linkClass} > - <a + <Link + to={link} className={rowClass} - href={'#'} - onClick={handleClick} > {icon} {status} {channel.display_name} {badge} {closeButton} - </a> + </Link> {tutorialTip} </li> ); @@ -600,6 +581,7 @@ export default class Sidebar extends React.Component { <div className='sidebar--left' id='sidebar-left' + key='sidebar-left' > <NewChannelFlow show={showChannelModal} diff --git a/webapp/components/sidebar_right.jsx b/webapp/components/sidebar_right.jsx index a2e3914f3..594674929 100644 --- a/webapp/components/sidebar_right.jsx +++ b/webapp/components/sidebar_right.jsx @@ -29,7 +29,7 @@ export default class SidebarRight extends React.Component { this.doStrangeThings = this.doStrangeThings.bind(this); this.state = { - searchVisible: !!SearchStore.getSearchResults(), + searchVisible: SearchStore.getSearchResults() !== null, isMentionSearch: SearchStore.getIsMentionSearch(), postRightVisible: !!PostStore.getSelectedPost(), fromSearch: false, @@ -111,7 +111,7 @@ export default class SidebarRight extends React.Component { } onSearchChange() { this.setState({ - searchVisible: !!SearchStore.getSearchResults(), + searchVisible: SearchStore.getSearchResults() !== null, isMentionSearch: SearchStore.getIsMentionSearch() }); } diff --git a/webapp/components/signup_team.jsx b/webapp/components/signup_team.jsx index e6b27e745..3ad47500d 100644 --- a/webapp/components/signup_team.jsx +++ b/webapp/components/signup_team.jsx @@ -138,6 +138,24 @@ export default class TeamSignUp extends React.Component { } let signupMethod = null; + let goBack = ( + <div className='signup-header'> + <a + href='#' + onClick={ + (e) => { + e.preventDefault(); + this.updatePage('choose'); + } + } + > + <span className='fa fa-chevron-left'/> + <FormattedMessage + id='web.header.back' + /> + </a> + </div> + ); if (global.window.mm_config.EnableTeamCreation !== 'true') { if (teamListing == null) { @@ -154,9 +172,12 @@ export default class TeamSignUp extends React.Component { updatePage={this.updatePage} /> ); + goBack = null; } else if (this.state.page === 'email') { signupMethod = ( - <EmailSignUpPage/> + <div> + <EmailSignUpPage/> + </div> ); } else if (this.state.page === 'ldap') { return ( @@ -180,24 +201,28 @@ export default class TeamSignUp extends React.Component { defaultMessage='No team creation method has been enabled. Please contact an administrator for access.' /> ); + goBack = null; } return ( - <div className='col-sm-12'> - <div className='signup-team__container'> - <img - className='signup-team-logo' - src={logoImage} - /> - <h1>{global.window.mm_config.SiteName}</h1> - <h4 className='color--light'> - <FormattedMessage - id='web.root.singup_info' + <div> + {goBack} + <div className='col-sm-12'> + <div className='signup-team__container'> + <img + className='signup-team-logo' + src={logoImage} /> - </h4> - <div id='signup-team'> - {teamListing} - {signupMethod} + <h1>{global.window.mm_config.SiteName}</h1> + <h4 className='color--light'> + <FormattedMessage + id='web.root.singup_info' + /> + </h4> + <div id='signup-team'> + {teamListing} + {signupMethod} + </div> </div> </div> </div> diff --git a/webapp/components/signup_team_complete/components/signup_team_complete.jsx b/webapp/components/signup_team_complete/components/signup_team_complete.jsx index 8b2096499..e5d310151 100644 --- a/webapp/components/signup_team_complete/components/signup_team_complete.jsx +++ b/webapp/components/signup_team_complete/components/signup_team_complete.jsx @@ -8,7 +8,7 @@ import {FormattedMessage} from 'react-intl'; import {browserHistory} from 'react-router'; import React from 'react'; -import Link from 'react-router'; +import {Link} from 'react-router'; export default class SignupTeamComplete extends React.Component { constructor(props) { @@ -55,7 +55,7 @@ export default class SignupTeamComplete extends React.Component { <div> <div className='signup-header'> <Link to='/'> - <span classNameName='fa fa-chevron-left'/> + <span className='fa fa-chevron-left'/> <FormattedMessage id='web.header.back'/> </Link> </div> diff --git a/webapp/components/signup_user_complete.jsx b/webapp/components/signup_user_complete.jsx index 5460daf29..e9f9d9d88 100644 --- a/webapp/components/signup_user_complete.jsx +++ b/webapp/components/signup_user_complete.jsx @@ -1,18 +1,21 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import ReactDOM from 'react-dom'; +import LoadingScreen from 'components/loading_screen.jsx'; +import LoginLdap from 'components/login/components/login_ldap.jsx'; + +import BrowserStore from 'stores/browser_store.jsx'; +import UserStore from 'stores/user_store.jsx'; + import * as Utils from 'utils/utils.jsx'; import * as Client from 'utils/client.jsx'; -import UserStore from 'stores/user_store.jsx'; -import BrowserStore from 'stores/browser_store.jsx'; import Constants from 'utils/constants.jsx'; -import LoadingScreen from 'components/loading_screen.jsx'; import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; import {browserHistory, Link} from 'react-router'; import React from 'react'; +import ReactDOM from 'react-dom'; import logoImage from 'images/logo.png'; @@ -314,13 +317,13 @@ class SignupUserComplete extends React.Component { </div> ); - var signupMessage = []; + let signupMessage = []; if (global.window.mm_config.EnableSignUpWithGitLab === 'true') { signupMessage.push( - <Link + <a className='btn btn-custom-login gitlab' key='gitlab' - to={'/api/v1/oauth/gitlab/signup' + window.location.search + '&team=' + encodeURIComponent(this.state.teamName)} + href={'/api/v1/oauth/gitlab/signup' + window.location.search + '&team=' + encodeURIComponent(this.state.teamName)} > <span className='icon'/> <span> @@ -329,16 +332,16 @@ class SignupUserComplete extends React.Component { defaultMessage='with GitLab' /> </span> - </Link> + </a> ); } if (global.window.mm_config.EnableSignUpWithGoogle === 'true') { signupMessage.push( - <Link + <a className='btn btn-custom-login google' key='google' - to={'/api/v1/oauth/google/signup' + window.location.search + '&team=' + encodeURIComponent(this.state.teamName)} + href={'/api/v1/oauth/google/signup' + window.location.search + '&team=' + encodeURIComponent(this.state.teamName)} > <span className='icon'/> <span> @@ -347,11 +350,26 @@ class SignupUserComplete extends React.Component { defaultMessage='with Google' /> </span> - </Link> + </a> ); } - var emailSignup; + let ldapSignup; + if (global.window.mm_config.EnableLdap === 'true' && global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP) { + ldapSignup = ( + <div className='inner__content'> + <h5><strong> + <FormattedMessage + id='signup_user_completed.withLdap' + defaultMessage='With your LDAP credentials' + /> + </strong></h5> + <LoginLdap teamName={this.state.teamName}/> + </div> + ); + } + + let emailSignup; if (global.window.mm_config.EnableSignUpWithEmail === 'true') { emailSignup = ( <div> @@ -397,24 +415,24 @@ class SignupUserComplete extends React.Component { {passwordError} </div> </div> + <p className='margin--extra'> + <button + type='submit' + onClick={this.handleSubmit} + className='btn-primary btn' + > + <FormattedMessage + id='signup_user_completed.create' + defaultMessage='Create Account' + /> + </button> + </p> </div> - <p className='margin--extra'> - <button - type='submit' - onClick={this.handleSubmit} - className='btn-primary btn' - > - <FormattedMessage - id='signup_user_completed.create' - defaultMessage='Create Account' - /> - </button> - </p> </div> ); } - if (signupMessage.length > 0 && emailSignup) { + if (signupMessage.length > 0 && (emailSignup || ldapSignup)) { signupMessage = ( <div> {signupMessage} @@ -428,7 +446,21 @@ class SignupUserComplete extends React.Component { ); } - if (signupMessage.length === 0 && !emailSignup) { + if (ldapSignup && emailSignup) { + ldapSignup = ( + <div> + {ldapSignup} + <div className='or__container'> + <FormattedMessage + id='signup_user_completed.or' + defaultMessage='or' + /> + </div> + </div> + ); + } + + if (signupMessage.length === 0 && !emailSignup && !ldapSignup) { emailSignup = ( <div> <FormattedMessage @@ -449,7 +481,7 @@ class SignupUserComplete extends React.Component { </div> <div className='col-sm-12'> <div className='signup-team__container padding--less'> - <form> + <div> <img className='signup-team-logo' src={logoImage} @@ -477,9 +509,10 @@ class SignupUserComplete extends React.Component { /> </h4> {signupMessage} + {ldapSignup} {emailSignup} {serverError} - </form> + </div> </div> </div> </div> diff --git a/webapp/components/spinner_button.jsx b/webapp/components/spinner_button.jsx index fcc9af8cd..becf395c5 100644 --- a/webapp/components/spinner_button.jsx +++ b/webapp/components/spinner_button.jsx @@ -14,20 +14,10 @@ export default class SpinnerButton extends React.Component { }; } - constructor(props) { - super(props); - - this.handleClick = this.handleClick.bind(this); - } - - handleClick(e) { - if (this.props.onClick) { - this.props.onClick(e); - } - } - render() { - if (this.props.spinning) { + const {spinning, children, ...props} = this.props; // eslint-disable-line no-use-before-define + + if (spinning) { return ( <img className='spinner-button__gif' @@ -38,10 +28,10 @@ export default class SpinnerButton extends React.Component { return ( <button - onClick={this.handleClick} - className='btn btn-sm btn-primary' + className='btn btn-primary' + {...props} > - {this.props.children} + {children} </button> ); } diff --git a/webapp/components/suggestion/search_suggestion_list.jsx b/webapp/components/suggestion/search_suggestion_list.jsx index b15cc4243..57aaee8ff 100644 --- a/webapp/components/suggestion/search_suggestion_list.jsx +++ b/webapp/components/suggestion/search_suggestion_list.jsx @@ -2,6 +2,7 @@ // See License.txt for license information. import $ from 'jquery'; +import React from 'react'; import ReactDOM from 'react-dom'; import Constants from 'utils/constants.jsx'; import SuggestionList from './suggestion_list.jsx'; diff --git a/webapp/components/team_general_tab.jsx b/webapp/components/team_general_tab.jsx index 776c84b48..c27e8ca59 100644 --- a/webapp/components/team_general_tab.jsx +++ b/webapp/components/team_general_tab.jsx @@ -84,6 +84,7 @@ class GeneralTab extends React.Component { } updateSection(section) { + $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update'); this.setState(this.setupInitialState(this.props)); this.props.updateSection(section); } diff --git a/webapp/components/team_settings_modal.jsx b/webapp/components/team_settings_modal.jsx index c19787993..657643367 100644 --- a/webapp/components/team_settings_modal.jsx +++ b/webapp/components/team_settings_modal.jsx @@ -62,6 +62,7 @@ class TeamSettingsModal extends React.Component { } } updateSection(section) { + $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update'); this.setState({activeSection: section}); } render() { diff --git a/webapp/components/team_signup_with_sso.jsx b/webapp/components/team_signup_with_sso.jsx index 9a46b2d6b..78396eea8 100644 --- a/webapp/components/team_signup_with_sso.jsx +++ b/webapp/components/team_signup_with_sso.jsx @@ -64,7 +64,7 @@ class SSOSignUpPage extends React.Component { this.props.service, (data) => { if (data.follow_link) { - browserHistory.push(data.follow_link); + window.location.href = data.follow_link; } else { browserHistory.push('/' + team.name + '/channels/town-square'); } diff --git a/webapp/components/textbox.jsx b/webapp/components/textbox.jsx index 371c581e5..c77e1f9a3 100644 --- a/webapp/components/textbox.jsx +++ b/webapp/components/textbox.jsx @@ -130,8 +130,8 @@ export default class Textbox extends React.Component { const helpText = ( <div - style={{visibility: hasText ? 'visible' : 'hidden', opacity: hasText ? '0.5' : '0'}} - className='help_format_text' + style={{visibility: hasText ? 'visible' : 'hidden', opacity: hasText ? '0.3' : '0'}} + className='help__format-text' > <b> <FormattedMessage @@ -208,8 +208,8 @@ export default class Textbox extends React.Component { dangerouslySetInnerHTML={{__html: this.state.preview ? TextFormatting.formatText(this.props.messageText) : ''}} > </div> - {helpText} <div className='help__text'> + {helpText} {previewLink} <a target='_blank' diff --git a/webapp/components/tutorial/tutorial_intro_screens.jsx b/webapp/components/tutorial/tutorial_intro_screens.jsx index 913a30483..0358a6a65 100644 --- a/webapp/components/tutorial/tutorial_intro_screens.jsx +++ b/webapp/components/tutorial/tutorial_intro_screens.jsx @@ -2,7 +2,6 @@ // See License.txt for license information. import UserStore from 'stores/user_store.jsx'; -import ChannelStore from 'stores/channel_store.jsx'; import TeamStore from 'stores/team_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import * as Utils from 'utils/utils.jsx'; @@ -11,6 +10,7 @@ import * as AsyncClient from 'utils/async_client.jsx'; import Constants from 'utils/constants.jsx'; import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; +import {browserHistory} from 'react-router'; const Preferences = Constants.Preferences; @@ -19,6 +19,12 @@ const NUM_SCREENS = 3; import React from 'react'; export default class TutorialIntroScreens extends React.Component { + static get propTypes() { + return { + townSquare: React.PropTypes.object, + offTopic: React.PropTypes.object + }; + } constructor(props) { super(props); @@ -34,7 +40,7 @@ export default class TutorialIntroScreens extends React.Component { return; } - Utils.switchChannel(ChannelStore.getByName(Constants.DEFAULT_CHANNEL)); + browserHistory.push(TeamStore.getCurrentTeamUrl() + '/channels/town-square'); const step = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 0); @@ -52,6 +58,8 @@ export default class TutorialIntroScreens extends React.Component { UserStore.getCurrentId(), '999' ); + + browserHistory.push(TeamStore.getCurrentTeamUrl() + '/channels/town-square'); } createScreen() { switch (this.state.currentScreen) { @@ -151,6 +159,11 @@ export default class TutorialIntroScreens extends React.Component { ); } + let townSquareDisplayName = Constants.DEFAULT_CHANNEL_UI_NAME; + if (this.props.townSquare) { + townSquareDisplayName = this.props.townSquare.display_name; + } + return ( <div> <h3> @@ -169,7 +182,10 @@ export default class TutorialIntroScreens extends React.Component { {supportInfo} <FormattedMessage id='tutorial_intro.end' - defaultMessage='Click βNextβ to enter Town Square. This is the first channel teammates see when they sign up. Use it for posting updates everyone needs to know.' + defaultMessage='Click βNextβ to enter {channel}. This is the first channel teammates see when they sign up. Use it for posting updates everyone needs to know.' + values={{ + channel: townSquareDisplayName + }} /> {circles} </div> diff --git a/webapp/components/tutorial/tutorial_tip.jsx b/webapp/components/tutorial/tutorial_tip.jsx index 3508e29a2..deca70794 100644 --- a/webapp/components/tutorial/tutorial_tip.jsx +++ b/webapp/components/tutorial/tutorial_tip.jsx @@ -15,6 +15,9 @@ import {Overlay} from 'react-bootstrap'; import React from 'react'; +import tutorialGif from 'images/tutorialTip.gif'; +import tutorialGifWhite from 'images/tutorialTipWhite.gif'; + export default class TutorialTip extends React.Component { constructor(props) { super(props); @@ -90,16 +93,16 @@ export default class TutorialTip extends React.Component { } } - var tipColor = ''; + var tutorialGifImage = tutorialGif; if (this.props.overlayClass === 'tip-overlay--header' || this.props.overlayClass === 'tip-overlay--sidebar') { - tipColor = 'White'; + tutorialGifImage = tutorialGifWhite; } return ( <div className={'tip-div ' + this.props.overlayClass}> <img className='tip-button' - src={'/static/images/tutorialTip' + tipColor + '.gif'} + src={tutorialGifImage} width='35' onClick={this.toggle} ref='target' diff --git a/webapp/components/tutorial/tutorial_view.jsx b/webapp/components/tutorial/tutorial_view.jsx new file mode 100644 index 000000000..5f2c1a257 --- /dev/null +++ b/webapp/components/tutorial/tutorial_view.jsx @@ -0,0 +1,44 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import TutorialIntroScreens from './tutorial_intro_screens.jsx'; + +import ChannelStore from 'stores/channel_store.jsx'; +import Constants from 'utils/constants.jsx'; + +import React from 'react'; + +export default class TutorialView extends React.Component { + constructor(props) { + super(props); + + this.handleChannelChange = this.handleChannelChange.bind(this); + + this.state = { + townSquare: ChannelStore.getByName(Constants.DEFAULT_CHANNEL) + }; + } + componentDidMount() { + ChannelStore.addChangeListener(this.handleChannelChange); + } + componentWillUnmount() { + ChannelStore.removeChangeListener(this.handleChannelChange); + } + handleChannelChange() { + this.setState({ + townSquare: ChannelStore.getByName(Constants.DEFAULT_CHANNEL) + }); + } + render() { + return ( + <div + id='app-content' + className='app__content' + > + <TutorialIntroScreens + townSquare={this.state.townSquare} + /> + </div> + ); + } +} diff --git a/webapp/components/user_settings/manage_incoming_hooks.jsx b/webapp/components/user_settings/manage_incoming_hooks.jsx deleted file mode 100644 index b61b331ce..000000000 --- a/webapp/components/user_settings/manage_incoming_hooks.jsx +++ /dev/null @@ -1,225 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import * as Client from 'utils/client.jsx'; -import * as Utils from 'utils/utils.jsx'; -import Constants from 'utils/constants.jsx'; -import ChannelStore from 'stores/channel_store.jsx'; -import LoadingScreen from '../loading_screen.jsx'; - -import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; - -import React from 'react'; - -export default class ManageIncomingHooks extends React.Component { - constructor() { - super(); - - this.getHooks = this.getHooks.bind(this); - this.addNewHook = this.addNewHook.bind(this); - this.updateChannelId = this.updateChannelId.bind(this); - - this.state = {hooks: [], channelId: ChannelStore.getByName(Constants.DEFAULT_CHANNEL).id, getHooksComplete: false}; - } - componentDidMount() { - this.getHooks(); - } - addNewHook() { - const hook = {}; - hook.channel_id = this.state.channelId; - - Client.addIncomingHook( - hook, - (data) => { - let hooks = this.state.hooks; - if (!hooks) { - hooks = []; - } - hooks.push(data); - this.setState({hooks}); - }, - (err) => { - this.setState({serverError: err}); - } - ); - } - removeHook(id) { - const data = {}; - data.id = id; - - Client.deleteIncomingHook( - data, - () => { - const hooks = this.state.hooks; - let index = -1; - for (let i = 0; i < hooks.length; i++) { - if (hooks[i].id === id) { - index = i; - break; - } - } - - if (index !== -1) { - hooks.splice(index, 1); - } - - this.setState({hooks}); - }, - (err) => { - this.setState({serverError: err}); - } - ); - } - getHooks() { - Client.listIncomingHooks( - (data) => { - const state = this.state; - - if (data) { - state.hooks = data; - } - - state.getHooksComplete = true; - this.setState(state); - }, - (err) => { - this.setState({serverError: err}); - } - ); - } - updateChannelId(e) { - this.setState({channelId: e.target.value}); - } - render() { - let serverError; - if (this.state.serverError) { - serverError = <label className='has-error'>{this.state.serverError}</label>; - } - - const channels = ChannelStore.getAll(); - const options = []; - channels.forEach((channel) => { - if (channel.type !== Constants.DM_CHANNEL) { - options.push( - <option - key={'incoming-hook' + channel.id} - value={channel.id} - > - {channel.display_name} - </option> - ); - } - }); - - let disableButton = ''; - if (this.state.channelId === '') { - disableButton = ' disable'; - } - - const hooks = []; - this.state.hooks.forEach((hook) => { - const c = ChannelStore.get(hook.channel_id); - if (c) { - hooks.push( - <div - key={hook.id} - className='webhook__item' - > - <div className='padding-top x2 webhook__url'> - <strong>{'URL: '}</strong> - <span className='word-break--all'>{Utils.getWindowLocationOrigin() + '/hooks/' + hook.id}</span> - </div> - <div className='padding-top'> - <strong> - <FormattedMessage - id='user.settings.hooks_in.channel' - defaultMessage='Channel: ' - /> - </strong>{c.display_name} - </div> - <a - className={'webhook__remove'} - href='#' - onClick={this.removeHook.bind(this, hook.id)} - > - <span aria-hidden='true'>{'Γ'}</span> - </a> - <div className='padding-top x2 divider-light'></div> - </div> - ); - } - }); - - let displayHooks; - if (!this.state.getHooksComplete) { - displayHooks = <LoadingScreen/>; - } else if (hooks.length > 0) { - displayHooks = hooks; - } else { - displayHooks = ( - <div className='padding-top x2'> - <FormattedMessage - id='user.settings.hooks_in.none' - defaultMessage='None' - /> - </div> - ); - } - - const existingHooks = ( - <div className='webhooks__container'> - <label className='control-label padding-top x2'> - <FormattedMessage - id='user.settings.hooks_in.existing' - defaultMessage='Existing incoming webhooks' - /> - </label> - <div className='padding-top divider-light'></div> - <div className='webhooks__list'> - {displayHooks} - </div> - </div> - ); - - return ( - <div key='addIncomingHook'> - <FormattedHTMLMessage - id='user.settings.hooks_in.description' - defaultMessage='Create webhook URLs for use in external integrations. Please see <a href="http://docs.mattermost.com/developer/webhooks-incoming.html" target="_blank">incoming webhooks documentation</a> to learn more. View all incoming webhooks configured on this team below.' - /> - <div><label className='control-label padding-top x2'> - <FormattedMessage - id='user.settings.hooks_in.addTitle' - defaultMessage='Add a new incoming webhook' - /> - </label></div> - <div className='row padding-top'> - <div className='col-sm-10 padding-bottom'> - <select - ref='channelName' - className='form-control' - value={this.state.channelId} - onChange={this.updateChannelId} - > - {options} - </select> - {serverError} - </div> - <div className='col-sm-2 col-xs-4 no-padding--left padding-bottom'> - <a - className={'btn form-control no-padding btn-sm btn-primary' + disableButton} - href='#' - onClick={this.addNewHook} - > - <FormattedMessage - id='user.settings.hooks_in.add' - defaultMessage='Add' - /> - </a> - </div> - </div> - {existingHooks} - </div> - ); - } -} diff --git a/webapp/components/user_settings/manage_outgoing_hooks.jsx b/webapp/components/user_settings/manage_outgoing_hooks.jsx deleted file mode 100644 index 8adec09ce..000000000 --- a/webapp/components/user_settings/manage_outgoing_hooks.jsx +++ /dev/null @@ -1,397 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import LoadingScreen from '../loading_screen.jsx'; - -import ChannelStore from 'stores/channel_store.jsx'; - -import * as Client from 'utils/client.jsx'; -import Constants from 'utils/constants.jsx'; - -import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl'; - -const holders = defineMessages({ - optional: { - id: 'user.settings.hooks_out.optional', - defaultMessage: 'Optional if channel selected' - }, - callbackHolder: { - id: 'user.settings.hooks_out.callbackHolder', - defaultMessage: 'Each URL must start with http:// or https://' - }, - select: { - id: 'user.settings.hooks_out.select', - defaultMessage: '--- Select a channel ---' - } -}); - -import React from 'react'; - -class ManageOutgoingHooks extends React.Component { - constructor() { - super(); - - this.getHooks = this.getHooks.bind(this); - this.addNewHook = this.addNewHook.bind(this); - this.updateChannelId = this.updateChannelId.bind(this); - this.updateTriggerWords = this.updateTriggerWords.bind(this); - this.updateCallbackURLs = this.updateCallbackURLs.bind(this); - - this.state = {hooks: [], channelId: '', triggerWords: '', callbackURLs: '', getHooksComplete: false}; - } - componentDidMount() { - this.getHooks(); - } - addNewHook(e) { - e.preventDefault(); - - if ((this.state.channelId === '' && this.state.triggerWords === '') || - this.state.callbackURLs === '') { - return; - } - - const hook = {}; - hook.channel_id = this.state.channelId; - if (this.state.triggerWords.length !== 0) { - hook.trigger_words = this.state.triggerWords.trim().split(','); - } - hook.callback_urls = this.state.callbackURLs.split('\n').map((url) => url.trim()); - - Client.addOutgoingHook( - hook, - (data) => { - let hooks = Object.assign([], this.state.hooks); - if (!hooks) { - hooks = []; - } - hooks.push(data); - this.setState({hooks, addError: null, channelId: '', triggerWords: '', callbackURLs: ''}); - }, - (err) => { - this.setState({addError: err.message}); - } - ); - } - removeHook(id) { - const data = {}; - data.id = id; - - Client.deleteOutgoingHook( - data, - () => { - const hooks = this.state.hooks; - let index = -1; - for (let i = 0; i < hooks.length; i++) { - if (hooks[i].id === id) { - index = i; - break; - } - } - - if (index !== -1) { - hooks.splice(index, 1); - } - - this.setState({hooks}); - }, - (err) => { - this.setState({editError: err.message}); - } - ); - } - regenToken(id) { - const regenData = {}; - regenData.id = id; - - Client.regenOutgoingHookToken( - regenData, - (data) => { - const hooks = Object.assign([], this.state.hooks); - for (let i = 0; i < hooks.length; i++) { - if (hooks[i].id === id) { - hooks[i] = data; - break; - } - } - - this.setState({hooks, editError: null}); - }, - (err) => { - this.setState({editError: err.message}); - } - ); - } - getHooks() { - Client.listOutgoingHooks( - (data) => { - if (data) { - this.setState({hooks: data, getHooksComplete: true, editError: null}); - } - }, - (err) => { - this.setState({editError: err.message}); - } - ); - } - updateChannelId(e) { - this.setState({channelId: e.target.value}); - } - updateTriggerWords(e) { - this.setState({triggerWords: e.target.value}); - } - updateCallbackURLs(e) { - this.setState({callbackURLs: e.target.value}); - } - render() { - let addError; - if (this.state.addError) { - addError = <label className='has-error'>{this.state.addError}</label>; - } - let editError; - if (this.state.editError) { - addError = <label className='has-error'>{this.state.editError}</label>; - } - - const channels = ChannelStore.getAll(); - const options = []; - options.push( - <option - key='select-channel' - value='' - > - {this.props.intl.formatMessage(holders.select)} - </option> - ); - - channels.forEach((channel) => { - if (channel.type === Constants.OPEN_CHANNEL) { - options.push( - <option - key={'outgoing-hook' + channel.id} - value={channel.id} - > - {channel.display_name} - </option> - ); - } - }); - - const hooks = []; - this.state.hooks.forEach((hook) => { - const c = ChannelStore.get(hook.channel_id); - - if (!c && hook.channel_id && hook.channel_id.length !== 0) { - return; - } - - let channelDiv; - if (c) { - channelDiv = ( - <div className='padding-top'> - <strong> - <FormattedMessage - id='user.settings.hooks_out.channel' - defaultMessage='Channel: ' - /> - </strong>{c.display_name} - </div> - ); - } - - let triggerDiv; - if (hook.trigger_words && hook.trigger_words.length !== 0) { - triggerDiv = ( - <div className='padding-top'> - <strong> - <FormattedMessage - id='user.settings.hooks_out.trigger' - defaultMessage='Trigger Words: ' - /> - </strong>{hook.trigger_words.join(', ')} - </div> - ); - } - - hooks.push( - <div - key={hook.id} - className='webhook__item' - > - <div className='padding-top x2 webhook__url'> - <strong>{'URLs: '}</strong><span className='word-break--all'>{hook.callback_urls.join(', ')}</span> - </div> - {channelDiv} - {triggerDiv} - <div className='padding-top'> - <strong>{'Token: '}</strong>{hook.token} - </div> - <div className='padding-top'> - <a - className='text-danger' - href='#' - onClick={this.regenToken.bind(this, hook.id)} - > - <FormattedMessage - id='user.settings.hooks_out.regen' - defaultMessage='Regen Token' - /> - </a> - <a - className='webhook__remove' - href='#' - onClick={this.removeHook.bind(this, hook.id)} - > - <span aria-hidden='true'>{'Γ'}</span> - </a> - </div> - <div className='padding-top x2 divider-light'></div> - </div> - ); - }); - - let displayHooks; - if (!this.state.getHooksComplete) { - displayHooks = <LoadingScreen/>; - } else if (hooks.length > 0) { - displayHooks = hooks; - } else { - displayHooks = ( - <div className='padding-top x2'> - <FormattedMessage - id='user.settings.hooks_out.none' - defaultMessage='None' - /> - </div> - ); - } - - const existingHooks = ( - <div className='webhooks__container'> - <label className='control-label padding-top x2'> - <FormattedMessage - id='user.settings.hooks_out.existing' - defaultMessage='Existing outgoing webhooks' - /> - </label> - <div className='padding-top divider-light'></div> - <div className='webhooks__list'> - {displayHooks} - </div> - </div> - ); - - const disableButton = (this.state.channelId === '' && this.state.triggerWords === '') || this.state.callbackURLs === ''; - - return ( - <div key='addOutgoingHook'> - <FormattedHTMLMessage - id='user.settings.hooks_out.addDescription' - defaultMessage='Create webhooks to send new message events to an external integration. Please see <a href="http://docs.mattermost.com/developer/webhooks-outgoing.html" target="_blank">outgoing webhooks documentation</a> to learn more. View all outgoing webhooks configured on this team below.' - /> - <div><label className='control-label padding-top x2'> - <FormattedMessage - id='user.settings.hooks_out.addTitle' - defaultMessage='Add a new outgoing webhook' - /> - </label></div> - <div className='padding-top divider-light'></div> - <div className='padding-top'> - <div> - <label className='control-label'> - <FormattedMessage - id='user.settings.hooks_out.channel' - defaultMessage='Channel: ' - /> - </label> - <div className='padding-top'> - <select - ref='channelName' - className='form-control' - value={this.state.channelId} - onChange={this.updateChannelId} - > - {options} - </select> - </div> - <div className='padding-top'> - <FormattedMessage - id='user.settings.hooks_out.only' - defaultMessage='Only public channels can be used' - /> - </div> - </div> - <div className='padding-top x2'> - <label className='control-label'> - <FormattedMessage - id='user.settings.hooks_out.trigger' - defaultMessage='Trigger Words: ' - /> - </label> - <div className='padding-top'> - <input - ref='triggerWords' - className='form-control' - value={this.state.triggerWords} - onChange={this.updateTriggerWords} - placeholder={this.props.intl.formatMessage(holders.optional)} - /> - </div> - <div className='padding-top'> - <FormattedMessage - id='user.settings.hooks_out.comma' - defaultMessage='Comma separated words to trigger on' - /> - </div> - </div> - <div className='padding-top x2'> - <label className='control-label'> - <FormattedMessage - id='user.settings.hooks_out.callback' - defaultMessage='Callback URLs: ' - /> - </label> - <div className='padding-top'> - <textarea - ref='callbackURLs' - className='form-control no-resize' - value={this.state.callbackURLs} - resize={false} - rows={3} - onChange={this.updateCallbackURLs} - placeholder={this.props.intl.formatMessage(holders.callbackHolder)} - /> - </div> - <div className='padding-top'> - <FormattedMessage - id='user.settings.hooks_out.callbackDesc' - defaultMessage='New line separated URLs that will receive the HTTP POST event' - /> - </div> - {addError} - </div> - <div className='padding-top padding-bottom'> - <a - className={'btn btn-sm btn-primary'} - href='#' - disabled={disableButton} - onClick={this.addNewHook} - > - <FormattedMessage - id='user.settings.hooks_out.add' - defaultMessage='Add' - /> - </a> - </div> - </div> - {existingHooks} - {editError} - </div> - ); - } -} - -ManageOutgoingHooks.propTypes = { - intl: intlShape.isRequired -}; - -export default injectIntl(ManageOutgoingHooks); diff --git a/webapp/components/user_settings/premade_theme_chooser.jsx b/webapp/components/user_settings/premade_theme_chooser.jsx index c35748b41..326120957 100644 --- a/webapp/components/user_settings/premade_theme_chooser.jsx +++ b/webapp/components/user_settings/premade_theme_chooser.jsx @@ -7,6 +7,8 @@ import Constants from 'utils/constants.jsx'; import React from 'react'; +import {FormattedMessage} from 'react-intl'; + export default class PremadeThemeChooser extends React.Component { constructor(props) { super(props); @@ -50,6 +52,17 @@ export default class PremadeThemeChooser extends React.Component { return ( <div className='row appearance-section'> {premadeThemes} + <div className='col-sm-12 padding-bottom x2'> + <a + href='http://docs.mattermost.com/help/settings/theme-colors.html#custom-themes' + target='_blank' + > + <FormattedMessage + id='user.settings.display.theme.otherThemes' + defaultMessage='See other themes' + /> + </a> + </div> </div> ); } diff --git a/webapp/components/user_settings/user_settings_advanced.jsx b/webapp/components/user_settings/user_settings_advanced.jsx index 4fcdc9a41..61e0e1dad 100644 --- a/webapp/components/user_settings/user_settings_advanced.jsx +++ b/webapp/components/user_settings/user_settings_advanced.jsx @@ -1,6 +1,7 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import $ from 'jquery'; import * as AsyncClient from 'utils/async_client.jsx'; import SettingItemMin from '../setting_item_min.jsx'; import SettingItemMax from '../setting_item_max.jsx'; @@ -151,6 +152,7 @@ class AdvancedSettingsDisplay extends React.Component { } updateSection(section) { + $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update'); this.props.updateSection(section); } diff --git a/webapp/components/user_settings/user_settings_display.jsx b/webapp/components/user_settings/user_settings_display.jsx index e56156049..d169e01b5 100644 --- a/webapp/components/user_settings/user_settings_display.jsx +++ b/webapp/components/user_settings/user_settings_display.jsx @@ -1,6 +1,7 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import $ from 'jquery'; import SettingItemMin from '../setting_item_min.jsx'; import SettingItemMax from '../setting_item_max.jsx'; import ManageLanguages from './manage_languages.jsx'; @@ -83,6 +84,7 @@ export default class UserSettingsDisplay extends React.Component { this.setState({selectedFont}); } updateSection(section) { + $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update'); this.updateState(); this.props.updateSection(section); } @@ -302,7 +304,7 @@ export default class UserSettingsDisplay extends React.Component { describe = ( <FormattedMessage id='user.settings.display.showUsername' - defaultMessage='Show username (team default)' + defaultMessage='Show username (default)' /> ); } else if (this.state.nameFormat === 'full_name') { diff --git a/webapp/components/user_settings/user_settings_general.jsx b/webapp/components/user_settings/user_settings_general.jsx index 2129847aa..eddbc1efe 100644 --- a/webapp/components/user_settings/user_settings_general.jsx +++ b/webapp/components/user_settings/user_settings_general.jsx @@ -1,6 +1,7 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import $ from 'jquery'; import SettingItemMin from '../setting_item_min.jsx'; import SettingItemMax from '../setting_item_max.jsx'; import SettingPicture from '../setting_picture.jsx'; @@ -13,7 +14,7 @@ import Constants from 'utils/constants.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import * as Utils from 'utils/utils.jsx'; -import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedDate} from 'react-intl'; +import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage, FormattedDate} from 'react-intl'; const holders = defineMessages({ usernameReserved: { @@ -36,18 +37,6 @@ const holders = defineMessages({ id: 'user.settings.general.checkEmail', defaultMessage: 'Check your email at {email} to verify the address.' }, - newAddress: { - id: 'user.settings.general.newAddress', - defaultMessage: 'New Address: {email}<br />Check your email to verify the above address.' - }, - checkEmailNoAddress: { - id: 'user.settings.general.checkEmailNoAddress', - defaultMessage: 'Check your email to verify your new address' - }, - loginGitlab: { - id: 'user.settings.general.loginGitlab', - defaultMessage: 'Log in done through GitLab' - }, validImage: { id: 'user.settings.general.validImage', defaultMessage: 'Only JPG or PNG images may be used for profile pictures' @@ -72,10 +61,6 @@ const holders = defineMessages({ id: 'user.settings.general.username', defaultMessage: 'Username' }, - email: { - id: 'user.settings.general.email', - defaultMessage: 'Email' - }, profilePicture: { id: 'user.settings.general.profilePicture', defaultMessage: 'Profile Picture' @@ -286,6 +271,7 @@ class UserSettingsGeneralTab extends React.Component { } } updateSection(section) { + $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update'); const emailChangeInProgress = this.state.emailChangeInProgress; this.setState(Object.assign({}, this.setupInitialState(this.props), {emailChangeInProgress, clientError: '', serverError: '', emailError: ''})); this.submitActive = false; @@ -297,9 +283,224 @@ class UserSettingsGeneralTab extends React.Component { return {username: user.username, firstName: user.first_name, lastName: user.last_name, nickname: user.nickname, email: user.email, confirmEmail: '', picture: null, loadingPicture: false, emailChangeInProgress: false}; } + createEmailSection() { + let emailSection; + + if (this.props.activeSection === 'email') { + const emailEnabled = global.window.mm_config.SendEmailNotifications === 'true'; + const emailVerificationEnabled = global.window.mm_config.RequireEmailVerification === 'true'; + const inputs = []; + + let helpText = ( + <FormattedMessage + id='user.settings.general.emailHelp1' + defaultMessage='Email is used for sign-in, notifications, and password reset. Email requires verification if changed.' + /> + ); + + if (!emailEnabled) { + helpText = ( + <div className='setting-list__hint text-danger'> + <FormattedMessage + id='user.settings.general.emailHelp2' + defaultMessage='Email has been disabled by your system administrator. No notification emails will be sent until it is enabled.' + /> + </div> + ); + } else if (!emailVerificationEnabled) { + helpText = ( + <FormattedMessage + id='user.settings.general.emailHelp3' + defaultMessage='Email is used for sign-in, notifications, and password reset.' + /> + ); + } else if (this.state.emailChangeInProgress) { + const newEmail = UserStore.getCurrentUser().email; + if (newEmail) { + helpText = ( + <FormattedMessage + id='user.settings.general.emailHelp4' + defaultMessage='A verification email was sent to {email}.' + values={{ + email: newEmail + }} + /> + ); + } + } + + let submit = null; + + if (this.props.user.auth_service === '') { + inputs.push( + <div key='emailSetting'> + <div className='form-group'> + <label className='col-sm-5 control-label'> + <FormattedMessage + id='user.settings.general.primaryEmail' + defaultMessage='Primary Email' + /> + </label> + <div className='col-sm-7'> + <input + className='form-control' + type='text' + onChange={this.updateEmail} + value={this.state.email} + /> + </div> + </div> + </div> + ); + + inputs.push( + <div key='confirmEmailSetting'> + <div className='form-group'> + <label className='col-sm-5 control-label'> + <FormattedMessage + id='user.settings.general.confirmEmail' + defaultMessage='Confirm Email' + /> + </label> + <div className='col-sm-7'> + <input + className='form-control' + type='text' + onChange={this.updateConfirmEmail} + value={this.state.confirmEmail} + /> + </div> + </div> + {helpText} + </div> + ); + + submit = this.submitEmail; + } else if (this.props.user.auth_service === Constants.GITLAB_SERVICE) { + inputs.push( + <div + key='oauthEmailInfo' + className='form-group' + > + <div className='setting-list__hint'> + <FormattedMessage + id='user.settings.general.emailGitlabCantUpdate' + defaultMessage='Login occurs through GitLab. Email cannot be updated. Email address used for notifications is {email}.' + values={{ + email: this.state.email + }} + /> + </div> + {helpText} + </div> + ); + } else if (this.props.user.auth_service === Constants.LDAP_SERVICE) { + inputs.push( + <div + key='oauthEmailInfo' + className='form-group' + > + <div className='setting-list__hint'> + <FormattedMessage + id='user.settings.general.emailLdapCantUpdate' + defaultMessage='Login occurs through LDAP. Email cannot be updated. Email address used for notifications is {email}.' + values={{ + email: this.state.email + }} + /> + </div> + {helpText} + </div> + ); + } + + emailSection = ( + <SettingItemMax + title={ + <FormattedMessage + id='user.settings.general.email' + defaultMessage='Email' + /> + } + inputs={inputs} + submit={submit} + server_error={this.state.serverError} + client_error={this.state.emailError} + updateSection={(e) => { + this.updateSection(''); + e.preventDefault(); + }} + /> + ); + } else { + let describe = ''; + if (this.props.user.auth_service === '') { + if (this.state.emailChangeInProgress) { + const newEmail = UserStore.getCurrentUser().email; + if (newEmail) { + describe = ( + <FormattedHTMLMessage + id='user.settings.general.newAddress' + defaultMessage='New Address: {email}<br />Check your email to verify the above address.' + values={{ + email: newEmail + }} + /> + ); + } else { + describe = ( + <FormattedMessage + id='user.settings.general.checkEmailNoAddress' + defaultMessage='Check your email to verify your new address' + /> + ); + } + } else { + describe = UserStore.getCurrentUser().email; + } + } else if (this.props.user.auth_service === Constants.GITLAB_SERVICE) { + describe = ( + <FormattedMessage + id='user.settings.general.loginGitlab' + defaultMessage='Login done through GitLab ({email})' + values={{ + email: this.state.email + }} + /> + ); + } else if (this.props.user.auth_service === Constants.LDAP_SERVICE) { + describe = ( + <FormattedMessage + id='user.settings.general.loginLdap' + defaultMessage='Login done through LDAP ({email})' + values={{ + email: this.state.email + }} + /> + ); + } + + emailSection = ( + <SettingItemMin + title={ + <FormattedMessage + id='user.settings.general.email' + defaultMessage='Email' + /> + } + describe={describe} + updateSection={() => { + this.updateSection('email'); + }} + /> + ); + } + + return emailSection; + } render() { const user = this.props.user; - const {formatMessage, formatHTMLMessage} = this.props.intl; + const {formatMessage} = this.props.intl; let clientError = null; if (this.state.clientError) { @@ -309,10 +510,6 @@ class UserSettingsGeneralTab extends React.Component { if (this.state.serverError) { serverError = this.state.serverError; } - let emailError = null; - if (this.state.emailError) { - emailError = this.state.emailError; - } let nameSection; const inputs = []; @@ -407,20 +604,27 @@ class UserSettingsGeneralTab extends React.Component { /> ); } else { - let fullName = ''; + let describe = ''; if (user.first_name && user.last_name) { - fullName = user.first_name + ' ' + user.last_name; + describe = user.first_name + ' ' + user.last_name; } else if (user.first_name) { - fullName = user.first_name; + describe = user.first_name; } else if (user.last_name) { - fullName = user.last_name; + describe = user.last_name; + } else { + describe = ( + <FormattedMessage + id='user.settings.general.emptyName' + defaultMessage="Click 'Edit' to add your full name" + /> + ); } nameSection = ( <SettingItemMin title={formatMessage(holders.fullName)} - describe={fullName} + describe={describe} updateSection={() => { this.updateSection('name'); }} @@ -481,10 +685,22 @@ class UserSettingsGeneralTab extends React.Component { /> ); } else { + let describe = ''; + if (user.nickname) { + describe = user.nickname; + } else { + describe = ( + <FormattedMessage + id='user.settings.general.emptyNickname' + defaultMessage="Click 'Edit' to add a nickname" + /> + ); + } + nicknameSection = ( <SettingItemMin title={formatMessage(holders.nickname)} - describe={UserStore.getCurrentUser().nickname} + describe={describe} updateSection={() => { this.updateSection('nickname'); }} @@ -557,152 +773,7 @@ class UserSettingsGeneralTab extends React.Component { ); } - let emailSection; - if (this.props.activeSection === 'email') { - const emailEnabled = global.window.mm_config.SendEmailNotifications === 'true'; - const emailVerificationEnabled = global.window.mm_config.RequireEmailVerification === 'true'; - let helpText = ( - <FormattedMessage - id='user.settings.general.emailHelp1' - defaultMessage='Email is used for sign-in, notifications, and password reset. Email requires verification if changed.' - /> - ); - - if (!emailEnabled) { - helpText = ( - <div className='setting-list__hint text-danger'> - <FormattedMessage - id='user.settings.general.emailHelp2' - defaultMessage='Email has been disabled by your system administrator. No notification emails will be sent until it is enabled.' - /> - </div> - ); - } else if (!emailVerificationEnabled) { - helpText = ( - <FormattedMessage - id='user.settings.general.emailHelp3' - defaultMessage='Email is used for sign-in, notifications, and password reset.' - /> - ); - } else if (this.state.emailChangeInProgress) { - const newEmail = UserStore.getCurrentUser().email; - if (newEmail) { - helpText = ( - <FormattedMessage - id='user.settings.general.emailHelp4' - defaultMessage='A verification email was sent to {email}.' - values={{ - email: newEmail - }} - /> - ); - } - } - - let submit = null; - - if (this.props.user.auth_service === '') { - inputs.push( - <div key='emailSetting'> - <div className='form-group'> - <label className='col-sm-5 control-label'> - <FormattedMessage - id='user.settings.general.primaryEmail' - defaultMessage='Primary Email' - /> - </label> - <div className='col-sm-7'> - <input - className='form-control' - type='text' - onChange={this.updateEmail} - value={this.state.email} - /> - </div> - </div> - </div> - ); - - inputs.push( - <div key='confirmEmailSetting'> - <div className='form-group'> - <label className='col-sm-5 control-label'> - <FormattedMessage - id='user.settings.general.confirmEmail' - defaultMessage='Confirm Email' - /> - </label> - <div className='col-sm-7'> - <input - className='form-control' - type='text' - onChange={this.updateConfirmEmail} - value={this.state.confirmEmail} - /> - </div> - </div> - {helpText} - </div> - ); - - submit = this.submitEmail; - } else if (this.props.user.auth_service === Constants.GITLAB_SERVICE) { - inputs.push( - <div - key='oauthEmailInfo' - className='form-group' - > - <div className='setting-list__hint'> - <FormattedMessage - id='user.settings.general.emailCantUpdate' - defaultMessage='Log in occurs through GitLab. Email cannot be updated.' - /> - </div> - {helpText} - </div> - ); - } - - emailSection = ( - <SettingItemMax - title='Email' - inputs={inputs} - submit={submit} - server_error={serverError} - client_error={emailError} - updateSection={(e) => { - this.updateSection(''); - e.preventDefault(); - }} - /> - ); - } else { - let describe = ''; - if (this.props.user.auth_service === '') { - if (this.state.emailChangeInProgress) { - const newEmail = UserStore.getCurrentUser().email; - if (newEmail) { - describe = formatHTMLMessage(holders.newAddress, {email: newEmail}); - } else { - describe = formatMessage(holders.checkEmailNoAddress); - } - } else { - describe = UserStore.getCurrentUser().email; - } - } else if (this.props.user.auth_service === Constants.GITLAB_SERVICE) { - describe = formatMessage(holders.loginGitlab); - } - - emailSection = ( - <SettingItemMin - title={formatMessage(holders.email)} - describe={describe} - updateSection={() => { - this.updateSection('email'); - }} - /> - ); - } + const emailSection = this.createEmailSection(); let pictureSection; if (this.props.activeSection === 'picture') { diff --git a/webapp/components/user_settings/user_settings_integrations.jsx b/webapp/components/user_settings/user_settings_integrations.jsx index 94fc184bd..37081b863 100644 --- a/webapp/components/user_settings/user_settings_integrations.jsx +++ b/webapp/components/user_settings/user_settings_integrations.jsx @@ -1,31 +1,14 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import $ from 'jquery'; import SettingItemMin from '../setting_item_min.jsx'; import SettingItemMax from '../setting_item_max.jsx'; -import ManageIncomingHooks from './manage_incoming_hooks.jsx'; -import ManageOutgoingHooks from './manage_outgoing_hooks.jsx'; import ManageCommandHooks from './manage_command_hooks.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; const holders = defineMessages({ - inName: { - id: 'user.settings.integrations.incomingWebhooks', - defaultMessage: 'Incoming Webhooks' - }, - inDesc: { - id: 'user.settings.integrations.incomingWebhooksDescription', - defaultMessage: 'Manage your incoming webhooks' - }, - outName: { - id: 'user.settings.integrations.outWebhooks', - defaultMessage: 'Outgoing Webhooks' - }, - outDesc: { - id: 'user.settings.integrations.outWebhooksDescription', - defaultMessage: 'Manage your outgoing webhooks' - }, cmdName: { id: 'user.settings.integrations.commands', defaultMessage: 'Slash Commands' @@ -47,77 +30,14 @@ class UserSettingsIntegrationsTab extends React.Component { this.state = {}; } updateSection(section) { + $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update'); this.props.updateSection(section); } render() { - let incomingHooksSection; - let outgoingHooksSection; let commandHooksSection; var inputs = []; const {formatMessage} = this.props.intl; - if (global.window.mm_config.EnableIncomingWebhooks === 'true') { - if (this.props.activeSection === 'incoming-hooks') { - inputs.push( - <ManageIncomingHooks key='incoming-hook-ui'/> - ); - - incomingHooksSection = ( - <SettingItemMax - title={formatMessage(holders.inName)} - width='medium' - inputs={inputs} - updateSection={(e) => { - this.updateSection(''); - e.preventDefault(); - }} - /> - ); - } else { - incomingHooksSection = ( - <SettingItemMin - title={formatMessage(holders.inName)} - width='medium' - describe={formatMessage(holders.inDesc)} - updateSection={() => { - this.updateSection('incoming-hooks'); - }} - /> - ); - } - } - - if (global.window.mm_config.EnableOutgoingWebhooks === 'true') { - if (this.props.activeSection === 'outgoing-hooks') { - inputs.push( - <ManageOutgoingHooks key='outgoing-hook-ui'/> - ); - - outgoingHooksSection = ( - <SettingItemMax - title={formatMessage(holders.outName)} - width='medium' - inputs={inputs} - updateSection={(e) => { - this.updateSection(''); - e.preventDefault(); - }} - /> - ); - } else { - outgoingHooksSection = ( - <SettingItemMin - title={formatMessage(holders.outName)} - width='medium' - describe={formatMessage(holders.outDesc)} - updateSection={() => { - this.updateSection('outgoing-hooks'); - }} - /> - ); - } - } - if (global.window.mm_config.EnableCommands === 'true') { if (this.props.activeSection === 'command-hooks') { inputs.push( @@ -185,10 +105,6 @@ class UserSettingsIntegrationsTab extends React.Component { /> </h3> <div className='divider-dark first'/> - {incomingHooksSection} - <div className='divider-light'/> - {outgoingHooksSection} - <div className='divider-dark'/> {commandHooksSection} <div className='divider-dark'/> </div> @@ -207,4 +123,4 @@ UserSettingsIntegrationsTab.propTypes = { collapseModal: React.PropTypes.func.isRequired }; -export default injectIntl(UserSettingsIntegrationsTab);
\ No newline at end of file +export default injectIntl(UserSettingsIntegrationsTab); diff --git a/webapp/components/user_settings/user_settings_modal.jsx b/webapp/components/user_settings/user_settings_modal.jsx index d1c1f0fe2..b71547baf 100644 --- a/webapp/components/user_settings/user_settings_modal.jsx +++ b/webapp/components/user_settings/user_settings_modal.jsx @@ -9,7 +9,6 @@ import SettingsSidebar from '../settings_sidebar.jsx'; import UserStore from 'stores/user_store.jsx'; import * as Utils from 'utils/utils.jsx'; -import Constants from 'utils/constants.jsx'; import {Modal} from 'react-bootstrap'; @@ -113,7 +112,6 @@ class UserSettingsModal extends React.Component { return; } - this.resetTheme(); this.deactivateTab(); this.props.onModalDismissed(); return; @@ -220,22 +218,10 @@ class UserSettingsModal extends React.Component { if (!skipConfirm && this.requireConfirm) { this.showConfirmModal(() => this.updateSection(section, true)); } else { - if (this.state.active_section === 'theme' && section !== 'theme') { - this.resetTheme(); - } this.setState({active_section: section}); } } - resetTheme() { - const user = UserStore.getCurrentUser(); - if (user.theme_props == null) { - Utils.applyTheme(Constants.THEMES.default); - } else { - Utils.applyTheme(user.theme_props); - } - } - render() { const {formatMessage} = this.props.intl; if (this.state.currentUser == null) { diff --git a/webapp/components/user_settings/user_settings_notifications.jsx b/webapp/components/user_settings/user_settings_notifications.jsx index fe2db6727..b119c42f9 100644 --- a/webapp/components/user_settings/user_settings_notifications.jsx +++ b/webapp/components/user_settings/user_settings_notifications.jsx @@ -1,6 +1,7 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import $ from 'jquery'; import ReactDOM from 'react-dom'; import SettingItemMin from '../setting_item_min.jsx'; import SettingItemMax from '../setting_item_max.jsx'; @@ -162,6 +163,7 @@ class NotificationsTab extends React.Component { this.updateState(); this.props.updateSection(''); e.preventDefault(); + $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update'); } updateSection(section) { this.updateState(); diff --git a/webapp/components/user_settings/user_settings_security.jsx b/webapp/components/user_settings/user_settings_security.jsx index 283d2c425..ff5a898a9 100644 --- a/webapp/components/user_settings/user_settings_security.jsx +++ b/webapp/components/user_settings/user_settings_security.jsx @@ -1,6 +1,7 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import $ from 'jquery'; import SettingItemMin from '../setting_item_min.jsx'; import SettingItemMax from '../setting_item_max.jsx'; import AccessHistoryModal from '../access_history_modal.jsx'; @@ -30,14 +31,6 @@ const holders = defineMessages({ id: 'user.settings.security.passwordMatchError', defaultMessage: 'The new passwords you entered do not match' }, - password: { - id: 'user.settings.security.password', - defaultMessage: 'Password' - }, - lastUpdated: { - id: 'user.settings.security.lastUpdated', - defaultMessage: 'Last updated {date} at {time}' - }, method: { id: 'user.settings.security.method', defaultMessage: 'Sign-in Method' @@ -55,12 +48,16 @@ class SecurityTab extends React.Component { super(props); this.submitPassword = this.submitPassword.bind(this); + this.activateMfa = this.activateMfa.bind(this); + this.deactivateMfa = this.deactivateMfa.bind(this); this.updateCurrentPassword = this.updateCurrentPassword.bind(this); this.updateNewPassword = this.updateNewPassword.bind(this); this.updateConfirmPassword = this.updateConfirmPassword.bind(this); + this.updateMfaToken = this.updateMfaToken.bind(this); this.getDefaultState = this.getDefaultState.bind(this); this.createPasswordSection = this.createPasswordSection.bind(this); this.createSignInSection = this.createSignInSection.bind(this); + this.showQrCode = this.showQrCode.bind(this); this.state = this.getDefaultState(); } @@ -69,7 +66,9 @@ class SecurityTab extends React.Component { currentPassword: '', newPassword: '', confirmPassword: '', - authService: this.props.user.auth_service + authService: this.props.user.auth_service, + mfaShowQr: false, + mfaToken: '' }; } submitPassword(e) { @@ -120,6 +119,51 @@ class SecurityTab extends React.Component { } ); } + activateMfa() { + const data = {}; + data.activate = true; + data.token = this.state.mfaToken; + + Client.updateMfa(data, + () => { + this.props.updateSection(''); + AsyncClient.getMe(); + this.setState(this.getDefaultState()); + }, + (err) => { + const state = this.getDefaultState(); + if (err.message) { + state.serverError = err.message; + } else { + state.serverError = err; + } + state.mfaError = ''; + this.setState(state); + } + ); + } + deactivateMfa() { + const data = {}; + data.activate = false; + + Client.updateMfa(data, + () => { + this.props.updateSection(''); + AsyncClient.getMe(); + this.setState(this.getDefaultState()); + }, + (err) => { + const state = this.getDefaultState(); + if (err.message) { + state.serverError = err.message; + } else { + state.serverError = err; + } + state.mfaError = ''; + this.setState(state); + } + ); + } updateCurrentPassword(e) { this.setState({currentPassword: e.target.value}); } @@ -129,123 +173,335 @@ class SecurityTab extends React.Component { updateConfirmPassword(e) { this.setState({confirmPassword: e.target.value}); } - createPasswordSection() { + updateMfaToken(e) { + this.setState({mfaToken: e.target.value}); + } + showQrCode(e) { + e.preventDefault(); + this.setState({mfaShowQr: true}); + } + createMfaSection() { let updateSectionStatus; - const {formatMessage} = this.props.intl; - - if (this.props.activeSection === 'password' && this.props.user.auth_service === '') { - const inputs = []; + let submit; + + if (this.props.activeSection === 'mfa') { + let content; + let extraInfo; + if (this.props.user.mfa_active) { + content = ( + <div key='mfaQrCode'> + <a + className='btn btn-primary' + href='#' + onClick={this.deactivateMfa} + > + <FormattedMessage + id='user.settings.mfa.remove' + defaultMessage='Remove MFA from your account' + /> + </a> + <br/> + </div> + ); - inputs.push( - <div - key='currentPasswordUpdateForm' - className='form-group' - > - <label className='col-sm-5 control-label'> + extraInfo = ( + <span> <FormattedMessage - id='user.settings.security.currentPassword' - defaultMessage='Current Password' - /> - </label> - <div className='col-sm-7'> - <input - className='form-control' - type='password' - onChange={this.updateCurrentPassword} - value={this.state.currentPassword} + id='user.settings.mfa.removeHelp' + defaultMessage='Removing multi-factor authentication will make your account more vulnerable to attacks.' /> + </span> + ); + } else if (this.state.mfaShowQr) { + content = ( + <div key='mfaButton'> + <label className='col-sm-5 control-label'> + <FormattedMessage + id='user.settings.mfa.qrCode' + defaultMessage='QR Code' + /> + </label> + <div className='col-sm-7'> + <img + className='qr-code-img' + src={'/api/v1/users/generate_mfa_qr?time=' + this.props.user.update_at} + /> + </div> + <br/> + <label className='col-sm-5 control-label'> + <FormattedMessage + id='user.settings.mfa.enterToken' + defaultMessage='Token' + /> + </label> + <div className='col-sm-7'> + <input + className='form-control' + type='text' + onChange={this.updateMfaToken} + value={this.state.mfaToken} + /> + </div> </div> - </div> - ); - inputs.push( - <div - key='newPasswordUpdateForm' - className='form-group' - > - <label className='col-sm-5 control-label'> + ); + + extraInfo = ( + <span> <FormattedMessage - id='user.settings.security.newPassword' - defaultMessage='New Password' - /> - </label> - <div className='col-sm-7'> - <input - className='form-control' - type='password' - onChange={this.updateNewPassword} - value={this.state.newPassword} + id='user.settings.mfa.addHelpQr' + defaultMessage='Please scan the QR code with the Google Authenticator app on your smartphone and fill in the token with one provided by the app.' /> + </span> + ); + + submit = this.activateMfa; + } else { + content = ( + <div key='mfaQrCode'> + <a + className='btn btn-primary' + href='#' + onClick={this.showQrCode} + > + <FormattedMessage + id='user.settings.mfa.add' + defaultMessage='Add MFA to your account' + /> + </a> + <br/> </div> - </div> - ); + ); + + extraInfo = ( + <span> + <FormattedMessage + id='user.settings.mfa.addHelp' + defaultMessage='To add multi-factor authentication to your account you must have a smartphone with Google Authenticator installed.' + /> + </span> + ); + } + + const inputs = []; inputs.push( <div - key='retypeNewPasswordUpdateForm' + key='mfaSetting' className='form-group' > - <label className='col-sm-5 control-label'> - <FormattedMessage - id='user.settings.security.retypePassword' - defaultMessage='Retype New Password' - /> - </label> - <div className='col-sm-7'> - <input - className='form-control' - type='password' - onChange={this.updateConfirmPassword} - value={this.state.confirmPassword} - /> - </div> + {content} </div> ); updateSectionStatus = function resetSection(e) { this.props.updateSection(''); - this.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null}); + this.setState({mfaToken: '', mfaShowQr: false, mfaError: null}); e.preventDefault(); }.bind(this); return ( <SettingItemMax - title={formatMessage(holders.password)} + title={Utils.localizeMessage('user.settings.mfa.title', 'Multi-factor Authentication')} inputs={inputs} - submit={this.submitPassword} + extraInfo={extraInfo} + submit={submit} server_error={this.state.serverError} - client_error={this.state.passwordError} + client_error={this.state.mfaError} updateSection={updateSectionStatus} /> ); } - var describe; - var d = new Date(this.props.user.last_password_update); + let describe; + if (this.props.user.mfa_active) { + describe = Utils.localizeMessage('user.settings.security.active', 'Active'); + } else { + describe = Utils.localizeMessage('user.settings.security.inactive', 'Inactive'); + } + + updateSectionStatus = function updateSection() { + this.props.updateSection('mfa'); + }.bind(this); - const hours12 = !Utils.isMilitaryTime(); - describe = ( - <FormattedMessage - id='user.settings.security.lastUpdated' - defaultMessage='Last updated {date} at {time}' - values={{ - date: ( - <FormattedDate - value={d} - day='2-digit' - month='short' - year='numeric' - /> - ), - time: ( - <FormattedTime - value={d} - hour12={hours12} - hour='2-digit' - minute='2-digit' - /> - ) - }} + return ( + <SettingItemMin + title={Utils.localizeMessage('user.settings.mfa.title', 'Multi-factor Authentication')} + describe={describe} + updateSection={updateSectionStatus} /> ); + } + createPasswordSection() { + let updateSectionStatus; + + if (this.props.activeSection === 'password') { + const inputs = []; + let submit; + + if (this.props.user.auth_service === '') { + submit = this.submitPassword; + + inputs.push( + <div + key='currentPasswordUpdateForm' + className='form-group' + > + <label className='col-sm-5 control-label'> + <FormattedMessage + id='user.settings.security.currentPassword' + defaultMessage='Current Password' + /> + </label> + <div className='col-sm-7'> + <input + className='form-control' + type='password' + onChange={this.updateCurrentPassword} + value={this.state.currentPassword} + /> + </div> + </div> + ); + inputs.push( + <div + key='newPasswordUpdateForm' + className='form-group' + > + <label className='col-sm-5 control-label'> + <FormattedMessage + id='user.settings.security.newPassword' + defaultMessage='New Password' + /> + </label> + <div className='col-sm-7'> + <input + className='form-control' + type='password' + onChange={this.updateNewPassword} + value={this.state.newPassword} + /> + </div> + </div> + ); + inputs.push( + <div + key='retypeNewPasswordUpdateForm' + className='form-group' + > + <label className='col-sm-5 control-label'> + <FormattedMessage + id='user.settings.security.retypePassword' + defaultMessage='Retype New Password' + /> + </label> + <div className='col-sm-7'> + <input + className='form-control' + type='password' + onChange={this.updateConfirmPassword} + value={this.state.confirmPassword} + /> + </div> + </div> + ); + } else if (this.props.user.auth_service === Constants.GITLAB_SERVICE) { + inputs.push( + <div + key='oauthEmailInfo' + className='form-group' + > + <div className='setting-list__hint'> + <FormattedMessage + id='user.settings.security.passwordGitlabCantUpdate' + defaultMessage='Login occurs through GitLab. Password cannot be updated.' + /> + </div> + </div> + ); + } else if (this.props.user.auth_service === Constants.LDAP_SERVICE) { + inputs.push( + <div + key='oauthEmailInfo' + className='form-group' + > + <div className='setting-list__hint'> + <FormattedMessage + id='user.settings.security.passwordLdapCantUpdate' + defaultMessage='Login occurs through LDAP. Password cannot be updated.' + /> + </div> + </div> + ); + } + + updateSectionStatus = function resetSection(e) { + this.props.updateSection(''); + this.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null}); + e.preventDefault(); + $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update'); + }.bind(this); + + return ( + <SettingItemMax + title={ + <FormattedMessage + id='user.settings.security.password' + defaultMessage='Password' + /> + } + inputs={inputs} + submit={submit} + server_error={this.state.serverError} + client_error={this.state.passwordError} + updateSection={updateSectionStatus} + /> + ); + } + + let describe; + + if (this.props.user.auth_service === '') { + const d = new Date(this.props.user.last_password_update); + const hours12 = !Utils.isMilitaryTime(); + + describe = ( + <FormattedMessage + id='user.settings.security.lastUpdated' + defaultMessage='Last updated {date} at {time}' + values={{ + date: ( + <FormattedDate + value={d} + day='2-digit' + month='short' + year='numeric' + /> + ), + time: ( + <FormattedTime + value={d} + hour12={hours12} + hour='2-digit' + minute='2-digit' + /> + ) + }} + /> + ); + } else if (this.props.user.auth_service === Constants.GITLAB_SERVICE) { + describe = ( + <FormattedMessage + id='user.settings.security.loginGitlab' + defaultMessage='Login done through Gitlab' + /> + ); + } else if (this.props.user.auth_service === Constants.LDAP_SERVICE) { + describe = ( + <FormattedMessage + id='user.settings.security.loginLdap' + defaultMessage='Login done through LDAP' + /> + ); + } updateSectionStatus = function updateSection() { this.props.updateSection('password'); @@ -253,7 +509,12 @@ class SecurityTab extends React.Component { return ( <SettingItemMin - title={formatMessage(holders.password)} + title={ + <FormattedMessage + id='user.settings.security.password' + defaultMessage='Password' + /> + } describe={describe} updateSection={updateSectionStatus} /> @@ -264,7 +525,6 @@ class SecurityTab extends React.Component { const user = this.props.user; if (this.props.activeSection === 'signin') { - const inputs = []; const teamName = TeamStore.getCurrent().name; let emailOption; @@ -346,6 +606,7 @@ class SecurityTab extends React.Component { ); } + const inputs = []; inputs.push( <div key='userSignInOption'> {emailOption} @@ -411,16 +672,22 @@ class SecurityTab extends React.Component { } render() { const passwordSection = this.createPasswordSection(); - let signInSection; let numMethods = 0; numMethods = global.window.mm_config.EnableSignUpWithGitLab === 'true' ? numMethods + 1 : numMethods; numMethods = global.window.mm_config.EnableSignUpWithGoogle === 'true' ? numMethods + 1 : numMethods; + numMethods = global.window.mm_config.EnableLdap === 'true' ? numMethods + 1 : numMethods; - if (global.window.mm_config.EnableSignUpWithEmail && numMethods > 0) { + let signInSection; + if (global.window.mm_config.EnableSignUpWithEmail === 'true' && numMethods > 0) { signInSection = this.createSignInSection(); } + let mfaSection; + if (global.window.mm_config.EnableMultifactorAuthentication === 'true' && global.window.mm_license.IsLicensed === 'true') { + mfaSection = this.createMfaSection(); + } + return ( <div> <div className='modal-header'> @@ -459,6 +726,8 @@ class SecurityTab extends React.Component { <div className='divider-dark first'/> {passwordSection} <div className='divider-light'/> + {mfaSection} + <div className='divider-light'/> {signInSection} <div className='divider-dark'/> <br></br> diff --git a/webapp/components/user_settings/user_settings_theme.jsx b/webapp/components/user_settings/user_settings_theme.jsx index 3414fe2e2..14991037d 100644 --- a/webapp/components/user_settings/user_settings_theme.jsx +++ b/webapp/components/user_settings/user_settings_theme.jsx @@ -40,7 +40,6 @@ export default class ThemeSetting extends React.Component { this.onChange = this.onChange.bind(this); this.submitTheme = this.submitTheme.bind(this); this.updateTheme = this.updateTheme.bind(this); - this.deactivate = this.deactivate.bind(this); this.resetFields = this.resetFields.bind(this); this.handleImportModal = this.handleImportModal.bind(this); @@ -62,12 +61,17 @@ export default class ThemeSetting extends React.Component { } } componentWillReceiveProps(nextProps) { - if (!this.props.selected && nextProps.selected) { + if (this.props.selected && !nextProps.selected) { this.resetFields(); } } componentWillUnmount() { UserStore.removeChangeListener(this.onChange); + + if (this.props.selected) { + const state = this.getStateFromStores(); + Utils.applyTheme(state.theme); + } } getStateFromStores() { const user = UserStore.getCurrentUser(); @@ -147,11 +151,6 @@ export default class ThemeSetting extends React.Component { updateType(type) { this.setState({type}); } - deactivate() { - const state = this.getStateFromStores(); - - Utils.applyTheme(state.theme); - } resetFields() { const state = this.getStateFromStores(); state.serverError = null; diff --git a/webapp/dispatcher/app_dispatcher.jsx b/webapp/dispatcher/app_dispatcher.jsx index dcc43129b..5e43d3ad7 100644 --- a/webapp/dispatcher/app_dispatcher.jsx +++ b/webapp/dispatcher/app_dispatcher.jsx @@ -8,6 +8,10 @@ const PayloadSources = Constants.PayloadSources; const AppDispatcher = Object.assign(new Flux.Dispatcher(), { handleServerAction: function performServerAction(action) { + if (!action.type) { + console.warning('handleServerAction called with undefined action type'); // eslint-disable-line no-console + } + var payload = { source: PayloadSources.SERVER_ACTION, action @@ -16,6 +20,10 @@ const AppDispatcher = Object.assign(new Flux.Dispatcher(), { }, handleViewAction: function performViewAction(action) { + if (!action.type) { + console.warning('handleViewAction called with undefined action type'); // eslint-disable-line no-console + } + var payload = { source: PayloadSources.VIEW_ACTION, action diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 00f4f333d..7dc6486ab 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -3,12 +3,17 @@ "about.date": "Build Date:", "about.enterpriseEditione1": "Enterprise Edition", "about.hash": "Build Hash:", + "about.copyright": "Copyright 2016 Mattermost, Inc. All rights reserved", "about.licensed": "Licensed by:", "about.number": "Build Number:", "about.teamEditiont0": "Team Edition", "about.teamEditiont1": "Enterprise Edition", "about.title": "About Mattermost", "about.version": "Version:", + "about.teamEditionSt": "All your team communication in one place, instantly searchable and accessible anywhere.", + "about.teamEditionLearn": "Join the Mattermost community at ", + "about.enterpriseEditionSt": "Modern enterprise communication from behind your firewall.", + "about.enterpriseEditionLearn": "Learn more about Enterprise Edition at ", "access_history.title": "Access History", "activity_log.activeSessions": "Active Sessions", "activity_log.browser": "Browser: {browser}", @@ -22,16 +27,40 @@ "activity_log_modal.android": "Android", "activity_log_modal.androidNativeApp": "Android Native App", "activity_log_modal.iphoneNativeApp": "iPhone Native App", + "add_incoming_webhook.cancel": "Cancel", + "add_incoming_webhook.channel": "Channel", + "add_incoming_webhook.channelRequired": "A valid channel is required", + "add_incoming_webhook.description": "Description", + "add_incoming_webhook.header": "Add Incoming Webhook", + "add_incoming_webhook.name": "Name", + "add_incoming_webhook.save": "Save", + "add_integration.header": "Add Integration", + "add_integration.incomingWebhook.description": "Create webhook URLs for use in external integrations.", + "add_integration.incomingWebhook.title": "Incoming Webhook", + "add_integration.outgoingWebhook.description": "Create webhooks to send new message events to an external integration.", + "add_integration.outgoingWebhook.title": "Outgoing Webhook", + "add_outgoing_webhook.callbackUrls": "Callback URLs (One Per Line)", + "add_outgoing_webhook.callbackUrlsRequired": "One or more callback URLs are required", + "add_outgoing_webhook.cancel": "Cancel", + "add_outgoing_webhook.channel": "Channel", + "add_outgoing_webhook.description": "Description", + "add_outgoing_webhook.header": "Add Outgoing Webhook", + "add_outgoing_webhook.name": "Name", + "add_outgoing_webhook.save": "Save", + "add_outgoing_webhook.triggerWOrds": "Trigger Words (One Per Line)", + "add_outgoing_webhook.triggerWords": "Trigger Words (One Per Line)", + "add_outgoing_webhook.triggerWordsOrChannelRequired": "A valid channel or a list of trigger words is required", "admin.audits.reload": "Reload", "admin.audits.title": "User Activity", "admin.compliance.directoryDescription": "Directory to which compliance reports are written. If blank, will be set to ./data/.", "admin.compliance.directoryExample": "Ex \"./data/\"", "admin.compliance.directoryTitle": "Compliance Directory Location:", + "admin.compliance.enableDailyDesc": "When true, Mattermost will generate a daily compliance report.", "admin.compliance.enableDailyTitle": "Enable Daily Report:", - "admin.compliance.enableDesc": "When true, Mattermost will generate a daily compliance report.", + "admin.compliance.enableDesc": "When true, Mattermost allows compliance reporting", "admin.compliance.enableTitle": "Enable Compliance:", "admin.compliance.false": "false", - "admin.compliance.noLicense": "<h4 class=\"banner__heading\">Note:</h4><p>Compliance is an enterprise feature. Your current license does not support Compliance. Click <a href=\"http://mattermost.com\" target=\"_blank\">here</a> for information and pricing on enterprise licenses.</p>", + "admin.compliance.noLicense": "<h4 class=\"banner__heading\">Note:</h4><p>Compliance is an enterprise feature. Your current license does not support Compliance. Click <a href=\"http://mattermost.com\"target=\"_blank\">here</a> for information and pricing on enterprise licenses.</p>", "admin.compliance.save": "Save", "admin.compliance.saving": "Saving Config...", "admin.compliance.title": "Compliance Settings", @@ -211,7 +240,7 @@ "admin.ldap.lastnameAttrDesc": "The attribute in the LDAP server that will be used to populate the last name of users in Mattermost.", "admin.ldap.lastnameAttrEx": "Ex \"sn\"", "admin.ldap.lastnameAttrTitle": "Last Name Attribute:", - "admin.ldap.noLicense": "<h4 class=\"banner__heading\">Note:</h4><p>LDAP is an enterprise feature. Your current license does not support LDAP. Click <a href=\"http://mattermost.com\" target=\"_blank\">here</a> for information and pricing on enterprise licenses.</p>", + "admin.ldap.noLicense": "<h4 class=\"banner__heading\">Note:</h4><p>LDAP is an enterprise feature. Your current license does not support LDAP. Click <a href=\"http://mattermost.com\"target=\"_blank\">here</a> for information and pricing on enterprise licenses.</p>", "admin.ldap.portDesc": "The port Mattermost will use to connect to the LDAP server. Default is 389.", "admin.ldap.portEx": "Ex \"389\"", "admin.ldap.portTitle": "LDAP Port:", @@ -229,7 +258,10 @@ "admin.ldap.usernameAttrEx": "Ex \"sAMAccountName\"", "admin.ldap.usernameAttrTitle": "Username Attribute:", "admin.licence.keyMigration": "If youβre migrating servers you may need to remove your license key from this server in order to install it on a new server. To start, <a href=\"http://mattermost.com\" target=\"_blank\">disable all Enterprise Edition features on this server</a>. This will enable the ability to remove the license key and downgrade this server from Enterprise Edition to Team Edition.", + "admin.license.choose": "Choose File", "admin.license.chooseFile": "Choose File", + "admin.license.edition": "Edition: ", + "admin.license.key": "License Key: ", "admin.license.keyRemove": "Remove Enterprise License and Downgrade Server", "admin.license.noFile": "No file uploaded", "admin.license.removing": "Removing License...", @@ -311,8 +343,8 @@ "admin.service.attemptTitle": "Maximum Login Attempts:", "admin.service.cmdsDesc": "When true, user created slash commands will be allowed.", "admin.service.cmdsTitle": "Enable Slash Commands: ", - "admin.service.corsDescription": "Enable HTTP Cross origin request from specific domains (separate by a spacebar). Use \"*\" if you want to allow CORS from any domain or leave it blank to disable it.", - "admin.service.corsEx": "http://example.com https://example.com", + "admin.service.corsDescription": "Enable HTTP Cross origin request from a specific domain. Use \"*\" if you want to allow CORS from any domain or leave it blank to disable it.", + "admin.service.corsEx": "http://example.com", "admin.service.corsTitle": "Allow Cross-origin Requests from:", "admin.service.developerDesc": "(Developer Option) When true, extra information around errors will be displayed in the UI.", "admin.service.developerTitle": "Enable Developer Mode: ", @@ -329,6 +361,8 @@ "admin.service.listenAddress": "Listen Address:", "admin.service.listenDescription": "The address to which to bind and listen. Entering \":8065\" will bind to all interfaces or you can choose one like \"127.0.0.1:8065\". Changing this will require a server restart before taking effect.", "admin.service.listenExample": "Ex \":8065\"", + "admin.service.mfaDesc": "When true, users will be given the option to add multi-factor authentication to their account. They will need a smartphone and an authenticator app such as Google Authenticator.", + "admin.service.mfaTitle": "Enable Multi-factor Authentication:", "admin.service.mobileSessionDays": "Session Length for Mobile Device in Days:", "admin.service.mobileSessionDaysDesc": "The native mobile session will expire after the number of days specified and will require a user to login again.", "admin.service.outWebhooksDesc": "When true, outgoing webhooks will be allowed.", @@ -488,6 +522,7 @@ "analytics.team.privateGroups": "Private Groups", "analytics.team.publicChannels": "Public Channels", "analytics.team.recentActive": "Recent Active Users", + "analytics.team.recentUsers": "Recent Active Users", "analytics.team.title": "Team Statistics for {team}", "analytics.team.totalPosts": "Total Posts", "analytics.team.totalUsers": "Total Users", @@ -550,6 +585,12 @@ "authorize.app": "The app <strong>{appName}</strong> would like the ability to access and modify your basic information.", "authorize.deny": "Deny", "authorize.title": "An application would like to connect to your {teamName} account", + "backstage_navbar.backToMattermost": "Back to {siteName}", + "backstage_sidebar.integrations": "Integrations", + "backstage_sidebar.integrations.add": "Add Integration", + "backstage_sidebar.integrations.add.incomingWebhook": "Incoming Webhook", + "backstage_sidebar.integrations.add.outgoingWebhook": "Outgoing Webhook", + "backstage_sidebar.integrations.installed": "Installed Integrations", "center_panel.recent": "Click here to jump to recent messages. ", "chanel_header.addMembers": "Add Members", "change_url.close": "Close", @@ -626,6 +667,7 @@ "channel_notifications.preferences": "Notification Preferences for ", "channel_notifications.sendDesktop": "Send desktop notifications", "channel_notifications.unreadInfo": "The channel name is bolded in the sidebar when there are unread messages. Selecting \"Only for mentions\" will bold the channel only when you are mentioned.", + "channel_select.placeholder": "--- Select a channel ---", "choose_auth_page.emailCreate": "Create new team with email address", "choose_auth_page.find": "Find my teams", "choose_auth_page.gitlabCreate": "Create new team with GitLab Account", @@ -671,6 +713,7 @@ "claim.oauth_to_email.pwdNotMatch": "Password do not match.", "claim.oauth_to_email.switchTo": "Switch {type} to email and password", "claim.oauth_to_email.title": "Switch {type} Account to Email", + "claim.oauth_to_email_newPwd": "Enter a new password for your {team} {site} account", "confirm_modal.cancel": "Cancel", "create_comment.addComment": "Add a comment...", "create_comment.comment": "Add Comment", @@ -732,8 +775,9 @@ "file_upload.filesAbove": "Files above {max}MB could not be uploaded: {filenames}", "file_upload.limited": "Uploads limited to {count} files maximum. Please use additional posts for more files.", "file_upload.pasted": "Image Pasted at ", - "filtered_user_list.count": "{count, number} {count, plural, one {member} other {members}}", - "filtered_user_list.countTotal": "{count, number} {count, plural, one {member} other {members}} of {total} Total", + "filtered_user_list.count": "{count} {count, plural, one {member} other {members}}", + "filtered_user_list.countTotal": "{count} {count, plural, one {member} other {members}} of {total} Total", + "filtered_user_list.member": "Member", "filtered_user_list.search": "Search members", "find_team.email": "Email", "find_team.findDescription": "An email was sent with links to any teams to which you are a member.", @@ -768,6 +812,16 @@ "get_team_invite_link_modal.help": "Send teammates the link below for them to sign-up to this team site. The Team Invite Link can be shared with multiple teammates as it does not change unless it's regenerated in Team Settings by a Team Admin.", "get_team_invite_link_modal.helpDisabled": "User creation has been disabled for your team. Please ask your team administrator for details.", "get_team_invite_link_modal.title": "Team Invite Link", + "installed_integrations.add": "Add Integration", + "installed_integrations.allFilter": "All ({count})", + "installed_integrations.delete": "Delete", + "installed_integrations.header": "Installed Integrations", + "installed_integrations.incomingWebhookType": "(Incoming Webhook)", + "installed_integrations.incomingWebhooksFilter": "Incoming Webhooks ({count})", + "installed_integrations.outgoingWebhookType": "(Outgoing Webhook)", + "installed_integrations.outgoingWebhooksFilter": "Outgoing Webhooks ({count})", + "installed_integrations.regenToken": "Regen Token", + "installed_integrations.search": "Search Integrations", "intro_messages.DM": "This is the start of your direct message history with {teammate}.<br />Direct messages and files shared here are not shown to people outside this area.", "intro_messages.anyMember": " Any member can join and read this channel.", "intro_messages.beginning": "Beginning of {name}", @@ -830,6 +884,10 @@ "login_ldap.pwdReq": "An LDAP password is required", "login_ldap.signin": "Sign in", "login_ldap.username": "LDAP Username", + "login_mfa.enterToken": "To complete the sign in process, please enter a token from your smartphone's authenticator", + "login_mfa.submit": "Submit", + "login_mfa.token": "MFA Token", + "login_mfa.tokenReq": "Please enter an MFA token", "login_username.badTeam": "Bad team name", "login_username.pwd": "Password", "login_username.pwdReq": "A password is required", @@ -873,6 +931,7 @@ "navbar_dropdown.console": "System Console", "navbar_dropdown.create": "Create a New Team", "navbar_dropdown.help": "Help", + "navbar_dropdown.integrations": "Integrations", "navbar_dropdown.inviteMember": "Invite New Member", "navbar_dropdown.logout": "Logout", "navbar_dropdown.manageMembers": "Manage Members", @@ -888,7 +947,7 @@ "password_form.title": "Password Reset", "password_form.update": "Your password has been updated successfully.", "password_send.checkInbox": "Please check your inbox.", - "password_send.description": "To reset your password, enter the email address you used to sign up.", + "password_send.description": "To reset your password, enter the email address you used to sign up", "password_send.email": "Email", "password_send.error": "Please enter a valid email address.", "password_send.link": "<p>A password reset link has been sent to <b>{email}</b></p>", @@ -985,7 +1044,7 @@ "sidebar.pg": "Private Groups", "sidebar.removeList": "Remove from list", "sidebar.tutorialScreen1": "<h4>Channels</h4><p><strong>Channels</strong> organize conversations across different topics. Theyβre open to everyone on your team. To send private communications use <strong>Direct Messages</strong> for a single person or <strong>Private Groups</strong> for multiple people.</p>", - "sidebar.tutorialScreen2": "<h4>\"Town Square\" and \"Off-Topic\" channels</h4><p>Here are two public channels to start:</p><p><strong>Town Square</strong> is a place for team-wide communication. Everyone in your team is a member of this channel.</p><p><strong>Off-Topic</strong> is a place for fun and humor outside of work-related channels. You and your team can decide what other channels to create.</p>", + "sidebar.tutorialScreen2": "<h4>\"{townsquare}\" and \"{offtopic}\" channels</h4><p>Here are two public channels to start:</p><p><strong>{townsquare}</strong> is a place for team-wide communication. Everyone in your team is a member of this channel.</p><p><strong>{offtopic}</strong> is a place for fun and humor outside of work-related channels. You and your team can decide what other channels to create.</p>", "sidebar.tutorialScreen3": "<h4>Creating and Joining Channels</h4><p>Click <strong>\"More...\"</strong> to create a new channel or join an existing one.</p><p>You can also create a new channel or private group by clicking the <strong>\"+\" symbol</strong> next to the channel or private group header.</p>", "sidebar.unreadAbove": "Unread post(s) above", "sidebar.unreadBelow": "Unread post(s) below", @@ -1027,6 +1086,7 @@ "signup_user_completed.validEmail": "Please enter a valid email address", "signup_user_completed.welcome": "Welcome to:", "signup_user_completed.whatis": "What's your email address?", + "signup_user_completed.withLdap": "With your LDAP credentials", "sso_signup.find": "Find my teams", "sso_signup.gitlab": "Create team with GitLab Account", "sso_signup.google": "Create team with Google Apps Account", @@ -1133,7 +1193,7 @@ "textbox.quote": ">quote", "textbox.strike": "strike", "tutorial_intro.allSet": "Youβre all set", - "tutorial_intro.end": "Click βNextβ to enter Town Square. This is the first channel teammates see when they sign up. Use it for posting updates everyone needs to know.", + "tutorial_intro.end": "Click βNextβ to enter {channel}. This is the first channel teammates see when they sign up. Use it for posting updates everyone needs to know.", "tutorial_intro.invite": "Invite teammates", "tutorial_intro.next": "Next", "tutorial_intro.screenOne": "<h3>Welcome to:</h3><h1>Mattermost</h1><p>Your team communication all in one place, instantly searchable and available anywhere</p><p>Keep your team connected to help them achieve what matters most.</p>", @@ -1238,6 +1298,7 @@ "user.settings.display.theme.customTheme": "Custom Theme", "user.settings.display.theme.describe": "Open to manage your theme", "user.settings.display.theme.import": "Import theme colors from Slack", + "user.settings.display.theme.otherThemes": "See other themes", "user.settings.display.theme.themeColors": "Theme Colors", "user.settings.display.theme.title": "Theme", "user.settings.display.title": "Display Settings", @@ -1246,18 +1307,22 @@ "user.settings.general.close": "Close", "user.settings.general.confirmEmail": "Confirm Email", "user.settings.general.email": "Email", - "user.settings.general.emailCantUpdate": "Log in occurs through GitLab. Email cannot be updated.", + "user.settings.general.emailGitlabCantUpdate": "Login occurs through GitLab. Email cannot be updated. Email address used for notifications is {email}.", "user.settings.general.emailHelp1": "Email is used for sign-in, notifications, and password reset. Email requires verification if changed.", "user.settings.general.emailHelp2": "Email has been disabled by your system administrator. No notification emails will be sent until it is enabled.", "user.settings.general.emailHelp3": "Email is used for sign-in, notifications, and password reset.", "user.settings.general.emailHelp4": "A verification email was sent to {email}.", + "user.settings.general.emailLdapCantUpdate": "Login occurs through LDAP. Email cannot be updated. Email address used for notifications is {email}.", "user.settings.general.emailMatch": "The new emails you entered do not match.", + "user.settings.general.emptyName": "Click 'Edit' to add your full name", + "user.settings.general.emptyNickname": "Click 'Edit' to add a nickname", "user.settings.general.firstName": "First Name", "user.settings.general.fullName": "Full Name", "user.settings.general.imageTooLarge": "Unable to upload profile image. File is too large.", "user.settings.general.imageUpdated": "Image last updated {date}", "user.settings.general.lastName": "Last Name", - "user.settings.general.loginGitlab": "Log in done through GitLab", + "user.settings.general.loginGitlab": "Login done through GitLab ({email})", + "user.settings.general.loginLdap": "Login done through LDAP ({email})", "user.settings.general.newAddress": "New Address: {email}<br />Check your email to verify the above address.", "user.settings.general.nickname": "Nickname", "user.settings.general.nicknameExtra": "Use Nickname for a name you might be called that is different from your first name and username. This is most often used when two or more people have similar sounding names and usernames.", @@ -1273,27 +1338,6 @@ "user.settings.general.usernameRestrictions": "Username must begin with a letter, and contain between {min} to {max} lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'.", "user.settings.general.validEmail": "Please enter a valid email address", "user.settings.general.validImage": "Only JPG or PNG images may be used for profile pictures", - "user.settings.hooks_in.add": "Add", - "user.settings.hooks_in.addTitle": "Add a new incoming webhook", - "user.settings.hooks_in.channel": "Channel: ", - "user.settings.hooks_in.description": "Create webhook URLs for use in external integrations. Please see <a href=\"http://docs.mattermost.com/developer/webhooks-incoming.html\" target=\"_blank\">incoming webhooks documentation</a> to learn more. View all incoming webhooks configured on this team below.", - "user.settings.hooks_in.existing": "Existing incoming webhooks", - "user.settings.hooks_in.none": "None", - "user.settings.hooks_out.add": "Add", - "user.settings.hooks_out.addDescription": "Create webhooks to send new message events to an external integration. Please see <a href=\"http://docs.mattermost.com/developer/webhooks-outgoing.html\" target=\"_blank\">outgoing webhooks documentation</a> to learn more. View all outgoing webhooks configured on this team below.", - "user.settings.hooks_out.addTitle": "Add a new outgoing webhook", - "user.settings.hooks_out.callback": "Callback URLs: ", - "user.settings.hooks_out.callbackDesc": "New line separated URLs that will receive the HTTP POST event", - "user.settings.hooks_out.callbackHolder": "Each URL must start with http:// or https://", - "user.settings.hooks_out.channel": "Channel: ", - "user.settings.hooks_out.comma": "Comma separated words to trigger on", - "user.settings.hooks_out.existing": "Existing outgoing webhooks", - "user.settings.hooks_out.none": "None", - "user.settings.hooks_out.only": "Only public channels can be used", - "user.settings.hooks_out.optional": "Optional if channel selected", - "user.settings.hooks_out.regen": "Regen Token", - "user.settings.hooks_out.select": "--- Select a channel ---", - "user.settings.hooks_out.trigger": "Trigger Words: ", "user.settings.import_theme.cancel": "Cancel", "user.settings.import_theme.importBody": "To import a theme, go to a Slack team and look for βPreferences -> Sidebar Themeβ. Open the custom theme option, copy the theme color values and paste them here:", "user.settings.import_theme.importHeader": "Import Slack Theme", @@ -1301,12 +1345,15 @@ "user.settings.import_theme.submitError": "Invalid format, please try copying and pasting in again.", "user.settings.integrations.commands": "Slash Commands", "user.settings.integrations.commandsDescription": "Manage your slash commands", - "user.settings.integrations.incomingWebhooks": "Incoming Webhooks", - "user.settings.integrations.incomingWebhooksDescription": "Manage your incoming webhooks", - "user.settings.integrations.outWebhooks": "Outgoing Webhooks", - "user.settings.integrations.outWebhooksDescription": "Manage your outgoing webhooks", "user.settings.integrations.title": "Integration Settings", "user.settings.languages.change": "Change interface language", + "user.settings.mfa.add": "Add MFA to your account", + "user.settings.mfa.addHelp": "To add multi-factor authentication to your account you must have a smartphone with Google Authenticator installed.", + "user.settings.mfa.addHelpQr": "Please scan the QR code with the Google Authenticator app on your smartphone and fill in the token with one provided by the app.", + "user.settings.mfa.enterToken": "Token", + "user.settings.mfa.qrCode": "QR Code", + "user.settings.mfa.remove": "Remove MFA from your account", + "user.settings.mfa.removeHelp": "Removing multi-factor authentication will make your account more vulnerable to attacks.", "user.settings.modal.advanced": "Advanced", "user.settings.modal.confirmBtns": "Yes, Discard", "user.settings.modal.confirmMsg": "You have unsaved changes, are you sure you want to discard them?", @@ -1347,18 +1394,22 @@ "user.settings.security.emailPwd": "Email and Password", "user.settings.security.gitlab": "GitLab SSO", "user.settings.security.lastUpdated": "Last updated {date} at {time}", + "user.settings.security.loginGitlab": "Login done through Gitlab", + "user.settings.security.loginLdap": "Login done through LDAP", "user.settings.security.logoutActiveSessions": "View and Logout of Active Sessions", "user.settings.security.method": "Sign-in Method", "user.settings.security.newPassword": "New Password", "user.settings.security.oneSignin": "You may only have one sign-in method at a time. Switching sign-in method will send an email notifying you if the change was successful.", "user.settings.security.password": "Password", + "user.settings.security.passwordGitlabCantUpdate": "Login occurs through GitLab. Password cannot be updated.", + "user.settings.security.passwordLdapCantUpdate": "Login occurs through LDAP. Password cannot be updated.", "user.settings.security.passwordLengthError": "New passwords must be at least {chars} characters", "user.settings.security.passwordMatchError": "The new passwords you entered do not match", "user.settings.security.retypePassword": "Retype New Password", "user.settings.security.switchEmail": "Switch to using email and password", "user.settings.security.switchGitlab": "Switch to using GitLab SSO", "user.settings.security.switchGoogle": "Switch to using Google SSO", - "user.settings.security.switchLda": "Switch to using LDAP", + "user.settings.security.switchLdap": "Switch to using LDAP", "user.settings.security.title": "Security Settings", "user.settings.security.viewHistory": "View Access History", "user_list.notFound": "No users found :(", diff --git a/webapp/i18n/es.json b/webapp/i18n/es.json index 20b79fc84..8cc9e5db6 100644 --- a/webapp/i18n/es.json +++ b/webapp/i18n/es.json @@ -22,13 +22,37 @@ "activity_log_modal.android": "Android", "activity_log_modal.androidNativeApp": "Android App Nativa", "activity_log_modal.iphoneNativeApp": "iPhone App Nativa", + "add_incoming_webhook.cancel": "Cancelar", + "add_incoming_webhook.channel": "Canal", + "add_incoming_webhook.channelRequired": "Es obligatorio asignar un canal vΓ‘lido", + "add_incoming_webhook.description": "DescripciΓ³n", + "add_incoming_webhook.header": "Agregar un Webhook de Entrada", + "add_incoming_webhook.name": "Nombre", + "add_incoming_webhook.save": "Guardar", + "add_integration.header": "Agregar IntegraciΓ³n", + "add_integration.incomingWebhook.description": "Crea webhook URLs para utilizarlas con integraciones externas.", + "add_integration.incomingWebhook.title": "Webhook de Entrada", + "add_integration.outgoingWebhook.description": "Crea webhooks para enviar mensajes a integraciones externas.", + "add_integration.outgoingWebhook.title": "Webhook de salida", + "add_outgoing_webhook.callbackUrls": "Callback URLs (Uno por LΓnea)", + "add_outgoing_webhook.callbackUrlsRequired": "Se require uno o mΓ‘s URLs para los callback", + "add_outgoing_webhook.cancel": "Cancelar", + "add_outgoing_webhook.channel": "Canal", + "add_outgoing_webhook.description": "DescripciΓ³n", + "add_outgoing_webhook.header": "Agregar Webhook de Salida", + "add_outgoing_webhook.name": "Nombre", + "add_outgoing_webhook.save": "Guardar", + "add_outgoing_webhook.triggerWOrds": "Palabras gatilladoras (Una por lΓnea)", + "add_outgoing_webhook.triggerWords": "Palabras gatilladoras (Una por LΓnea)", + "add_outgoing_webhook.triggerWordsOrChannelRequired": "Se require al menos un canal vΓ‘lido o una lista de palabras gatilladoras", "admin.audits.reload": "Recargar", "admin.audits.title": "AuditorΓas del Servidor", "admin.compliance.directoryDescription": "Directorio en el que se escriben los informes de cumplimiento. Si se deja en blanco, se utilizarΓ‘ ./data/.", "admin.compliance.directoryExample": "Ej \"./data/\"", "admin.compliance.directoryTitle": "UbicaciΓ³n del Directorio de Cumplimiento:", + "admin.compliance.enableDailyDesc": "Cuando es verdadero, Mattermost generarΓ‘ un reporte de cumplimiento diario.", "admin.compliance.enableDailyTitle": "Habilitar Informes Diarios:", - "admin.compliance.enableDesc": "Cuando es verdadero, Mattermost generarΓ‘ un informe diario de cumplimiento.", + "admin.compliance.enableDesc": "Cuando es verdadero, Mattermost permite la creaciΓ³n de reportes de cumplimiento", "admin.compliance.enableTitle": "Habilitar el Cumplimiento:", "admin.compliance.false": "falso", "admin.compliance.noLicense": "<h4 class=\"banner__heading\">Nota:</h4><p>El Cumplimiento es una caracterΓstica de la ediciΓ³n enterprise. Tu licencia actual no soporta Cumplimiento. Pincha <a href=\"http://mattermost.com\" target=\"_blank\">aquΓ</a> para informaciΓ³n y precio de las licencias enterprise.</p>", @@ -211,7 +235,7 @@ "admin.ldap.lastnameAttrDesc": "El atributo en el servidor LDAP que serΓ‘ utilizado para poblar el apellido de los usuarios en Mattermost.", "admin.ldap.lastnameAttrEx": "Ej \"sn\"", "admin.ldap.lastnameAttrTitle": "Atributo Apellido:", - "admin.ldap.noLicense": "<h4 class=\"banner__heading\">Nota:</h4><p>LDAP es una caracterΓstica de la ediciΓ³n enterprise. Tu licencia actual no soporta LDAP. Pincha <a href=\"http://mattermost.com\" target=\"_blank\">aquΓ</a> para obtener informaciΓ³n y precios de las licencias de la ediciΓ³n enterprise.</p>", + "admin.ldap.noLicense": "<h4 class=\"banner__heading\">Nota:</h4><p>LDAP es una caracterΓstica de la ediciΓ³n enterprise. Tu licencia actual no soporta LDAP. Pincha <a href=\"http://mattermost.com\" target=\"_blank\">aquΓ</a> para obtener informaciΓ³n y precios de las licencias enterprise.</p>", "admin.ldap.portDesc": "El puerto que Mattermost utilizarΓ‘ para conectarse al servidor LDAP. El predeterminado es 389.", "admin.ldap.portEx": "Ej \"389\"", "admin.ldap.portTitle": "Puerto LDAP:", @@ -229,7 +253,10 @@ "admin.ldap.usernameAttrEx": "Ej \"sAMAccountName\"", "admin.ldap.usernameAttrTitle": "Atributo Usuario:", "admin.licence.keyMigration": "Si estΓ‘s migrando servidores es posible que necesites remover tu licencia de este servidor para poder instalarlo en un servidor nuevo. Para empezar, <a href=\"http://mattermost.com\" target=\"_blank\">deshabilita todas las caracterΓsticas de la EdiciΓ³n Enterprise de este servidor</a>. Esta operaciΓ³n habilitarΓ‘ la opciΓ³n para remover la licencia y degradar este servidor de la EdiciΓ³n Enterprise a la EdiciΓ³n Team.", + "admin.license.choose": "Seleccionar Archivo", "admin.license.chooseFile": "Escoger Archivo", + "admin.license.edition": "EdiciΓ³n: ", + "admin.license.key": "Licencia: ", "admin.license.keyRemove": "Remover la Licencia Enterprise y Degradar el Servidor", "admin.license.noFile": "No se subiΓ³ ningΓΊn archivo", "admin.license.removing": "Removiendo Licencia...", @@ -311,8 +338,8 @@ "admin.service.attemptTitle": "MΓ‘ximo de intentos de conexiΓ³n:", "admin.service.cmdsDesc": "Cuando es verdadero, se permite la creaciΓ³n de comandos de barra por usuarios.", "admin.service.cmdsTitle": "Habilitar Comandos de Barra: ", - "admin.service.corsDescription": "Habilita las solicitudes HTTP de origen cruzado para dominios en especΓfico (separados por un espacio). Utiliza \"*\" si quieres habilitar CORS desde cualquier dominio o deja el campo en blanco para deshabilitarlo.", - "admin.service.corsEx": "http://ejemplo.com https://ejemplo.com", + "admin.service.corsDescription": "Habilitar solicitudes HTTP de origen cruzado desde un dominio especΓfico. Utiliza \"*\" si quieres permitir CORS desde cualquier dominio o dejalo en blanco para deshabilitarlo.", + "admin.service.corsEx": "http://ejemplo.com", "admin.service.corsTitle": "Permitir Solicitudes de Origen Cruzado desde:", "admin.service.developerDesc": "(OpciΓ³n de Desarrollador) Cuando estΓ‘ asignado en verdadero, informaciΓ³n extra sobre errores se muestra en el UI.", "admin.service.developerTitle": "Habilitar modo de Desarrollador: ", @@ -329,6 +356,8 @@ "admin.service.listenAddress": "DirecciΓ³n de escucha:", "admin.service.listenDescription": "La direcciΓ³n a la que se unirΓ‘ y escucharΓ‘. Ingresar \":8065\" se podrΓ‘ unir a todas las interfaces o podrΓ‘ seleccionar una como ej: \"127.0.0.1:8065\". Cambiando este valor es necesario reiniciar el servidor.", "admin.service.listenExample": "Ej \":8065\"", + "admin.service.mfaDesc": "Cuando es verdadero, los usuarios tendrΓ‘n la opciΓ³n de agregar autenticaciΓ³n de mΓΊltiples factores a sus cuentas. NecesitarΓ‘n un telΓ©fono inteligente y una app de autenticaciΓ³n como Google Authenticator.", + "admin.service.mfaTitle": "Habilitar AutenticaciΓ³n de MΓΊltiples Factores:", "admin.service.mobileSessionDays": "DuraciΓ³n de la SesiΓ³n en DΓas para Dispositivos Moviles:", "admin.service.mobileSessionDaysDesc": "La sesiΓ³n nativa de los dispositivos moviles expirarΓ‘ luego de transcurrido el numero de dΓas especificado y se solicitarΓ‘ al usuario que inicie sesiΓ³n nuevamente.", "admin.service.outWebhooksDesc": "Cuando es verdadero, los webhooks de salida serΓ‘n permitidos.", @@ -488,6 +517,7 @@ "analytics.team.privateGroups": "Grupos Privados", "analytics.team.publicChannels": "Canales PΓΊblicos", "analytics.team.recentActive": "Usuarios Recientemente Activos", + "analytics.team.recentUsers": "Usuarios Recientemente Activos", "analytics.team.title": "EstΓ‘disticas del Equipo {team}", "analytics.team.totalPosts": "Total de Mensajes", "analytics.team.totalUsers": "Total de Usuarios", @@ -550,6 +580,12 @@ "authorize.app": "La app <strong>{appName}</strong> quiere tener la habilidad de accesar y modificar tu informaciΓ³n bΓ‘sica.", "authorize.deny": "Denegar", "authorize.title": "Una aplicaciΓ³n quiere conectarse con tu cuenta de {teamName}", + "backstage_navbar.backToMattermost": "Volver a {siteName}", + "backstage_sidebar.integrations": "Integraciones", + "backstage_sidebar.integrations.add": "Agregar IntegraciΓ³n", + "backstage_sidebar.integrations.add.incomingWebhook": "Webhook de Entrada", + "backstage_sidebar.integrations.add.outgoingWebhook": "Webhook de Salida", + "backstage_sidebar.integrations.installed": "Integraciones Instaladas", "center_panel.recent": "Pincha aquΓ para ir a los mensajes mΓ‘s recientes. ", "chanel_header.addMembers": "Agregar Miembros", "change_url.close": "Cerrar", @@ -626,6 +662,7 @@ "channel_notifications.preferences": "Preferencias de NotificaciΓ³n para ", "channel_notifications.sendDesktop": "Enviar notificaciones de escritorio", "channel_notifications.unreadInfo": "El nombre del canal estΓ‘ en negritas en la barra lateral cuando hay mensajes sin leer. Al elegir \"SΓ³lo para menciones\" sΓ³lo lo dejarΓ‘ en negritas cuando seas mencionado.", + "channel_select.placeholder": "--- Selecciona un canal ---", "choose_auth_page.emailCreate": "Crea un nuevo equipo con tu cuenta de correo", "choose_auth_page.find": "Encontrar mi equipo", "choose_auth_page.gitlabCreate": "Crear un nuevo equipo con una cuenta de GitLab", @@ -671,6 +708,7 @@ "claim.oauth_to_email.pwdNotMatch": "Las contraseΓ±as no coinciden.", "claim.oauth_to_email.switchTo": "Cambiar {type} a correo electrΓ³nico y contraseΓ±a", "claim.oauth_to_email.title": "Cambiar la cuenta de {type} a Correo ElectrΓ³nico", + "claim.oauth_to_email_newPwd": "Ingresa una nueva contraseΓ±a para tu cuenta de {team} en {site}", "confirm_modal.cancel": "Cancelar", "create_comment.addComment": "Agregar un comentario...", "create_comment.comment": "Agregar Comentario", @@ -732,8 +770,9 @@ "file_upload.filesAbove": "No se pueden subir archivos de mΓ‘s de {max}MB: {filenames}", "file_upload.limited": "Se pueden subir un mΓ‘ximo de {count} archivos. Por favor envΓa otros mensajes para adjuntar mΓ‘s archivos.", "file_upload.pasted": "Imagen Pegada el ", - "filtered_user_list.count": "{count, number} {count, plural, one {Miembro} other {Miembros}}", - "filtered_user_list.countTotal": "{count, number} {count, plural, one {Miembro} other {Miembros}} de {total} Total", + "filtered_user_list.count": "{count} {count, plural, one {miembro} other {miembros}}", + "filtered_user_list.countTotal": "{count} {count, plural, one {miembro} other {miembros}} de {total} Total", + "filtered_user_list.member": "Miembro", "filtered_user_list.search": "Buscar miembros", "find_team.email": "Correo electrΓ³nico", "find_team.findDescription": "Enviamos un correo electrΓ³nico con los equipos a los que perteneces.", @@ -768,6 +807,16 @@ "get_team_invite_link_modal.help": "EnvΓa el siguiente enlace a tus compaΓ±eros para que se registren a este equipo. El enlace de invitaciΓ³n al equipo puede ser compartido con multiples compaΓ±eros y el mismo no cambiarΓ‘ a menos que sea regenerado en la ConfiguraciΓ³n del Equipo por un Administrador del Equipo.", "get_team_invite_link_modal.helpDisabled": "La creaciΓ³n de usuario ha sido deshabilitada para tu equipo. Por favor solicita mΓ‘s detalles a tu administrador de equipo.", "get_team_invite_link_modal.title": "Enlace de InvitaciΓ³n al Equipo", + "installed_integrations.add": "Agregar IntegraciΓ³n", + "installed_integrations.allFilter": "Todos ({count})", + "installed_integrations.delete": "Eliminar", + "installed_integrations.header": "Integraciones Instaladas", + "installed_integrations.incomingWebhookType": "(Webhook de Entrada)", + "installed_integrations.incomingWebhooksFilter": "Webhooks de Entrada ({count})", + "installed_integrations.outgoingWebhookType": "(Webhook de Salida)", + "installed_integrations.outgoingWebhooksFilter": "Webhooks de Salida ({count})", + "installed_integrations.regenToken": "Regenerar Token", + "installed_integrations.search": "Buscar Integraciones", "intro_messages.DM": "Este es el inicio de tu historial de mensajes directos con {teammate}.<br />Los mensajes directos y archivos que se comparten aquΓ no son mostrados a personas fuera de esta Γ‘rea.", "intro_messages.anyMember": " Cualquier miembro se puede unir y leer este canal.", "intro_messages.beginning": "Inicio de {name}", @@ -830,6 +879,10 @@ "login_ldap.pwdReq": "La contraseΓ±a LDAP es obligatoria", "login_ldap.signin": "Entrar", "login_ldap.username": "Usuario LDAP", + "login_mfa.enterToken": "Para completar el proceso de inicio de sesiΓ³n, por favor ingresa el token provisto por el autenticador de tu telΓ©fono inteligente", + "login_mfa.submit": "Enviar", + "login_mfa.token": "Token AMF", + "login_mfa.tokenReq": "Por favor ingresa un token AMF", "login_username.badTeam": "Mal nombre de equipo", "login_username.pwd": "ContraseΓ±a", "login_username.pwdReq": "La contraseΓ±a es obligatoria", @@ -873,6 +926,7 @@ "navbar_dropdown.console": "Consola de Sistema", "navbar_dropdown.create": "Crear nuevo Equipo", "navbar_dropdown.help": "Ayuda", + "navbar_dropdown.integrations": "Integraciones", "navbar_dropdown.inviteMember": "Invitar Nuevo Miembro", "navbar_dropdown.logout": "Cerrar sesiΓ³n", "navbar_dropdown.manageMembers": "Administrar Miembros", @@ -888,7 +942,7 @@ "password_form.title": "Restablecer ContraseΓ±a", "password_form.update": "Tu contraseΓ±a ha sido actualizada satisfactoriamente.", "password_send.checkInbox": "Por favor revisa tu bandeja de entrada.", - "password_send.description": "Para restablecer tu contraseΓ±a, ingresa la direcciΓ³n de correo electrΓ³nico que utilizaste para registrarte.", + "password_send.description": "Para reiniciar tu contraseΓ±a, ingresa la direcciΓ³n de correo que utilizaste en el registro", "password_send.email": "Correo electrΓ³nico", "password_send.error": "Por favor ingresa una direcciΓ³n correo electrΓ³nico vΓ‘lida.", "password_send.link": "<p>Se ha enviado un enlace para restablecer la contraseΓ±a a <b>{email}</b></p>", @@ -985,7 +1039,7 @@ "sidebar.pg": "Grupos Privados", "sidebar.removeList": "Remover de la lista", "sidebar.tutorialScreen1": "<h4>Canales</h4><p><strong>Canales</strong> organizan las conversaciones en diferentes tΓ³picos. Son abiertos para cualquier persona de tu equipo. Para enviar comunicaciones privadas con una sola persona utiliza <strong>Mensajes Directos</strong> o con multiples personas utilizando <strong>Grupos Privados</strong>.</p>", - "sidebar.tutorialScreen2": "<h4>Los canal \"General\" y \"Fuera de TΓ³pico\"</h4><p>Estos son dos canales para comenzar:</p><p><strong>General</strong> es el lugar para tener comunicaciΓ³n con todo el equipo. Todos los integrantes de tu equipo son miembros de este canal.</p><p><strong>Fuera de TΓ³pico</strong> es un lugar para diversiΓ³n y humor fuera de los canales relacionados con el trabajo. Tu y tu equipo pueden decidir que otros canales crear.</p>", + "sidebar.tutorialScreen2": "<h4>Los canal \"{townsquare}\" y \"{offtopic}\"</h4><p>Estos son dos canales para comenzar:</p><p><strong>{townsquare}</strong> es el lugar para tener comunicaciΓ³n con todo el equipo. Todos los integrantes de tu equipo son miembros de este canal.</p><p><strong>{offtopic}</strong> es un lugar para diversiΓ³n y humor fuera de los canales relacionados con el trabajo. Tu y tu equipo pueden decidir que otros canales crear.</p>", "sidebar.tutorialScreen3": "<h4>Creando y Uniendose a Canales</h4><p>Pincha en <strong>\"MΓ‘s...\"</strong> para crear un nuevo canal o unirte a uno existente.</p><p>TambiΓ©n puedes crear un nuevo canal o grupo privado al pinchar el simbolo de <strong>\"+\"</strong> que se encuentra al lado del encabezado de Canales o Grupos Privados.</p>", "sidebar.unreadAbove": "Mensaje(s) sin leer β²", "sidebar.unreadBelow": "Mensaje(s) sin leer βΌ", @@ -1027,6 +1081,7 @@ "signup_user_completed.validEmail": "Por favor ingresa una direcciΓ³n de correo electrΓ³nico vΓ‘lida", "signup_user_completed.welcome": "Bienvenido a:", "signup_user_completed.whatis": "ΒΏCuΓ‘l es tu direcciΓ³n de correo electrΓ³nico?", + "signup_user_completed.withLdap": "Con tus credenciales de LDAP", "sso_signup.find": "Encontrar mi equipo", "sso_signup.gitlab": "Crea un equipo con una cuenta de GitLab", "sso_signup.google": "Crea un equipo con una cuenta de Google Apps", @@ -1133,7 +1188,7 @@ "textbox.quote": ">cita", "textbox.strike": "tachado", "tutorial_intro.allSet": "Ya estΓ‘s listo para comenzar", - "tutorial_intro.end": "Pincha βSiguienteβ para entrar al Canal General. Este es el primer canal que ven tus compaΓ±eros cuando ingresan. Utilizalo para mandar mensajes que todos deben leer.", + "tutorial_intro.end": "Pincha βSiguienteβ para entrar al {channel}. Este es el primer canal que ven tus compaΓ±eros cuando ingresan. Utilizalo para mandar mensajes que todos deben leer.", "tutorial_intro.invite": "Invitar compaΓ±eros", "tutorial_intro.next": "Siguiente", "tutorial_intro.screenOne": "<h3>Bienvenido a:</h3> <h1>Mattermost</h1> <p>Las comunicaciones de tu equipo en un solo lugar, con bΓΊsquedas instantΓ‘neas y disponible desde donde sea.</p> <p>MantΓ©n a tu equipo conectado para ayudarlos a conseguir lo que realmente importa.</p>", @@ -1238,6 +1293,7 @@ "user.settings.display.theme.customTheme": "Tema Personalizado", "user.settings.display.theme.describe": "Abrir para administrar tu tema", "user.settings.display.theme.import": "Importar colores del tema desde Slack", + "user.settings.display.theme.otherThemes": "Ver otros temas", "user.settings.display.theme.themeColors": "Colores del Tema", "user.settings.display.theme.title": "Tema", "user.settings.display.title": "ConfiguraciΓ³n de VisualizaciΓ³n", @@ -1246,18 +1302,22 @@ "user.settings.general.close": "Cerrar", "user.settings.general.confirmEmail": "Confirmar Correo electrΓ³nico", "user.settings.general.email": "Correo electrΓ³nico", - "user.settings.general.emailCantUpdate": "El inicio de sesiΓ³n ocurre a travΓ©s de GitLab. El correo electrΓ³nico no puede ser cambiado.", + "user.settings.general.emailGitlabCantUpdate": "El inicio de sesiΓ³n ocurre a travΓ©s GitLab. El correo electrΓ³nico no puede ser actualizado. La direcciΓ³n de correo electrΓ³nico utilizada para las notificaciones es {email}.", "user.settings.general.emailHelp1": "El correo electrΓ³nico es utilizado para iniciar sesiΓ³n, recibir notificaciones y para restablecer la contraseΓ±a. Si se cambia el correo electrΓ³nico deberΓ‘s verificarlo nuevamente.", "user.settings.general.emailHelp2": "El correo ha sido deshabilitado por el administrador de sistemas. No llegarΓ‘n correos de notificaciΓ³n hasta que se vuelva a habilitar.", "user.settings.general.emailHelp3": "El correo electrΓ³nico es utilizado para iniciar sesiΓ³n, recibir notificaciones y para restablecer la contraseΓ±a.", "user.settings.general.emailHelp4": "Un correo de verificaciΓ³n ha sido enviado a {email}.", + "user.settings.general.emailLdapCantUpdate": "El inicio de sesiΓ³n ocurre a travΓ©s LDAP. El correo electrΓ³nico no puede ser actualizado. La direcciΓ³n de correo electrΓ³nico utilizada para las notificaciones es {email}.", "user.settings.general.emailMatch": "El nuevo correo electrΓ³nico introducido no coincide.", + "user.settings.general.emptyName": "Pincha 'Editar' para agregar tu nombre completo", + "user.settings.general.emptyNickname": "Pincha 'Edita' para agregar un sobrenombre", "user.settings.general.firstName": "Nombre", "user.settings.general.fullName": "Nombre completo", "user.settings.general.imageTooLarge": "No se puede subir la imagen del perfil. El archivo es muy grande.", "user.settings.general.imageUpdated": "Γltima actualizacΓ³n de la imagen {date}", "user.settings.general.lastName": "Apellido", - "user.settings.general.loginGitlab": "Inicio de sesiΓ³n realizado a travΓ©s de GitLab", + "user.settings.general.loginGitlab": "Inicio de sesiΓ³n realizado a travΓ©s de GitLab ({email})", + "user.settings.general.loginLdap": "Inicio de sesiΓ³n realizado a travΓ©s de LDAP ({email})", "user.settings.general.newAddress": "Nueva direcciΓ³n: {email}<br />Revisa tu correo electrΓ³nico para verificar tu nueva direcciΓ³n.", "user.settings.general.nickname": "Sobrenombre", "user.settings.general.nicknameExtra": "Utiliza un Sobrenombre por el cual te conocen que sea diferente de tu nombre y del nombre de tu usuario. Esto se utiliza con mayor frecuencia cuando dos o mΓ‘s personas tienen nombres y nombres de usuario que suenan similares.", @@ -1273,27 +1333,6 @@ "user.settings.general.usernameRestrictions": "El nombre de usuario debe empezar con una letra, y contener entre {min} a {max} caracteres en minΓΊscula con nΓΊmeros, lettras, y los sΓmbolos '.', '-' y '_'.", "user.settings.general.validEmail": "Por favor ingresa una direcciΓ³n de correo electrΓ³nico vΓ‘lida", "user.settings.general.validImage": "SΓ³lo pueden ser utilizadas imΓ‘genes JPG o PNG en el perfil", - "user.settings.hooks_in.add": "Agregar", - "user.settings.hooks_in.addTitle": "Agregar un nuevo webhook de entrada", - "user.settings.hooks_in.channel": "Canal: ", - "user.settings.hooks_in.description": "Crea URLs para webhooks a utilizar con integraciones externas. Revisa la <a href=\"http://docs.mattermost.com/developer/webhooks-incoming.html\" target=\"_blank\">documentaciΓ³n de webhooks de entrada</a> para conocer mΓ‘s. Ver todos los webhooks de entrada configurados para este equipo en la parte de abajo.", - "user.settings.hooks_in.existing": "Webhooks de entrada existentes", - "user.settings.hooks_in.none": "Ninguno", - "user.settings.hooks_out.add": "Agregar", - "user.settings.hooks_out.addDescription": "Crea webhooks para enviar mensajes a ingraciones externas. Revisa la <a href=\"http://docs.mattermost.com/developer/webhooks-outgoing.html\" target=\"_blank\">documentaciΓ³n de webhooks de saldida</a> para conocer mΓ‘s. Ver todos los webhooks de salida configurados para este equipo en la parte de abajo.", - "user.settings.hooks_out.addTitle": "Agregar un nuevo webhook de salida", - "user.settings.hooks_out.callback": "Callback URLs:", - "user.settings.hooks_out.callbackDesc": "Separa por una nueva linea cada URL donde quieres recibir el evento de HTTP POST", - "user.settings.hooks_out.callbackHolder": "Cada URL debe comenzar con http:// o https://", - "user.settings.hooks_out.channel": "Canal: ", - "user.settings.hooks_out.comma": "Escribe las palabras de activaciΓ³n que ejecutan el evento separadas por coma", - "user.settings.hooks_out.existing": "Webhooks de salida existentes", - "user.settings.hooks_out.none": "Ninguno", - "user.settings.hooks_out.only": "SΓ³lo se pueden utilizar Canales", - "user.settings.hooks_out.optional": "Opcional si se selecciona un canal", - "user.settings.hooks_out.regen": "Regenerar Token", - "user.settings.hooks_out.select": "--- Selecciona un canal ---", - "user.settings.hooks_out.trigger": "Palabras de activaciΓ³n: ", "user.settings.import_theme.cancel": "Cancelar", "user.settings.import_theme.importBody": "Para importar un tema, anda al equipo Slack y busca en [Preferences -> Sidebar Theme]. Abre las opciones del tema, copia los valores de color del tema y pΓ©galo aquΓ:", "user.settings.import_theme.importHeader": "Importar Tema de Slack", @@ -1301,12 +1340,15 @@ "user.settings.import_theme.submitError": "Formato invΓ‘lido, por favor intenta copiando y pegando nuevamente.", "user.settings.integrations.commands": "Comandos de Barra", "user.settings.integrations.commandsDescription": "Administra tus comandos de barra", - "user.settings.integrations.incomingWebhooks": "Webhooks de entrada", - "user.settings.integrations.incomingWebhooksDescription": "Administra tus webhooks de entrada", - "user.settings.integrations.outWebhooks": "Webhooks de salida", - "user.settings.integrations.outWebhooksDescription": "Administra tus webhooks de salida", "user.settings.integrations.title": "Configuraciones de IntegraciΓ³n", "user.settings.languages.change": "Cambia el idioma con el que se muestra la intefaz de usuario", + "user.settings.mfa.add": "Agrega AMF a tu cuenta", + "user.settings.mfa.addHelp": "Para agregar autenticaciΓ³n de mΓΊltiples factores a tu cuenta debes tener un telΓ©fono inteligente con Google Authenticator instalado.", + "user.settings.mfa.addHelpQr": "Por favor escanea el cΓ³digo QR con la app de Google Authenticator en tu telΓ©fono inteligente e ingresa el token provisto por la app.", + "user.settings.mfa.enterToken": "Token", + "user.settings.mfa.qrCode": "CΓ³digo QR", + "user.settings.mfa.remove": "Remover AMF de tu cuenta", + "user.settings.mfa.removeHelp": "Al remover la autenticaciΓ³n de mΓΊltples factores harΓ‘ que tu cuenta sea vulnerable a ataques.", "user.settings.modal.advanced": "Avanzada", "user.settings.modal.confirmBtns": "SΓ, Descartar", "user.settings.modal.confirmMsg": "Tienes cambios sin guardar, ΒΏEstΓ‘s seguro que los quieres descartar?", @@ -1347,18 +1389,22 @@ "user.settings.security.emailPwd": "Correo electrΓ³nico y ContraseΓ±a", "user.settings.security.gitlab": "GitLab SSO", "user.settings.security.lastUpdated": "Γltima actualizaciΓ³n {date} a las {time}", + "user.settings.security.loginGitlab": "Inicio de sesiΓ³n realizado a travΓ©s de Gitlab", + "user.settings.security.loginLdap": "Inicio de sesiΓ³n realizado a travΓ©s de LDAP", "user.settings.security.logoutActiveSessions": "Visualizar y cerrar las sesiones activas", "user.settings.security.method": "MΓ©todo de inicio de sesiΓ³n", "user.settings.security.newPassword": "Nueva ContraseΓ±a", "user.settings.security.oneSignin": "SΓ³lo puedes tener un mΓ©todo de inicio de sesiΓ³n a la vez. El cambio del mΓ©todo de inicio de sesiΓ³n te enviarΓ‘ un correo notificandote que el cambio se realizΓ³ con Γ©xito.", "user.settings.security.password": "ContraseΓ±a", + "user.settings.security.passwordGitlabCantUpdate": "El inicio de sesiΓ³n ocurre a travΓ©s GitLab. La contraseΓ±a no se puede actualizar.", + "user.settings.security.passwordLdapCantUpdate": "El inicio de sesiΓ³n ocurre a travΓ©s LDAP. La contraseΓ±a no se puede actualizar.", "user.settings.security.passwordLengthError": "La nueva contraseΓ±a debe contener al menos {chars} carΓ‘cteres", "user.settings.security.passwordMatchError": "La nueva contraseΓ±a que ingresaste no coincide", "user.settings.security.retypePassword": "Reescribe la Nueva ContraseΓ±a", "user.settings.security.switchEmail": "Cambiar para utilizar correo electrΓ³nico y contraseΓ±a", "user.settings.security.switchGitlab": "Cambiar para utilizar GitLab SSO", "user.settings.security.switchGoogle": "Cambiar para utilizar Google SSO", - "user.settings.security.switchLda": "Cambiar a utilizar LDAP", + "user.settings.security.switchLdap": "Cambiar a utilizar LDAP", "user.settings.security.title": "ConfiguraciΓ³n de Seguridad", "user.settings.security.viewHistory": "Visualizar historial de acceso", "user_list.notFound": "No se encontraron usuarios :(", diff --git a/webapp/i18n/fr.json b/webapp/i18n/fr.json index 3270b8847..be207f0da 100644 --- a/webapp/i18n/fr.json +++ b/webapp/i18n/fr.json @@ -986,7 +986,7 @@ "sidebar.pg": "Groupes privΓ©s", "sidebar.removeList": "Retirer de la liste", "sidebar.tutorialScreen1": "<h4>Canaux</h4><p><strong>Les canaux</strong> organisent les conversations en sujets distincts. Ils sont ouverts Γ tout le monde dans votre Γ©quipe. Pour envoyer des messages privΓ©s, utilisez <strong>Messages PrivΓ©s</strong> pour une personne ou <strong>Groupes PrivΓ©s</strong> pour plusieurs personnes.</p>", - "sidebar.tutorialScreen2": "<h4>Canaux \"Town Square\" et \"Off-Topic\"</h4><p>Voici deux canaux publics pour commencer :</p><p><strong>Town Square</strong> (\"centre-ville\") est l'endroit idΓ©al pour communiquer avec toute l'Γ©quipe. Tous les membres de votre Γ©quipe sont membres de ce canal.</p><p><strong>Off-Topic</strong> (\"hors-sujet\") est l'endroit pour se dΓ©tendre et parler d'autre chose que de travail. Vous et votre Γ©quipe dΓ©cidez des autres canaux Γ crΓ©er.</p>", + "sidebar.tutorialScreen2": "<h4>Canaux \"{townsquare}\" et \"{offtopic}\"</h4><p>Voici deux canaux publics pour commencer :</p><p><strong>{townsquare}</strong> (\"centre-ville\") est l'endroit idΓ©al pour communiquer avec toute l'Γ©quipe. Tous les membres de votre Γ©quipe sont membres de ce canal.</p><p><strong>{offtopic}</strong> (\"hors-sujet\") est l'endroit pour se dΓ©tendre et parler d'autre chose que de travail. Vous et votre Γ©quipe dΓ©cidez des autres canaux Γ crΓ©er.</p>", "sidebar.tutorialScreen3": "<h4>CrΓ©er et rejoindre des canaux</h4><p>Cliquez sur <strong>\"Plus...\"</strong> pour crΓ©er un nouveau canal ou rejoindre un canal existant.</p><p>Vous pouvez aussi crΓ©er un nouveau canal ou un groupe privΓ© en cliquant sur le symbole <strong>\"+\"</strong> Γ cΓ΄tΓ© du nom du canal ou de l'en-tΓͺte du groupe privΓ©.</p>", "sidebar.unreadAbove": "Message(s) non-lu(s) ci-dessus", "sidebar.unreadBelow": "Message(s) non-lu(s) ci-dessous", @@ -1134,7 +1134,7 @@ "textbox.quote": ">citation", "textbox.strike": "barrΓ©", "tutorial_intro.allSet": "C'est parti !", - "tutorial_intro.end": "Cliquez sur \"Suivant\" pour entrer dans Town Square. C'est le premier canal que les membres voient quand ils s'inscrivent. Utilisez-le pour poster des messages que tout le monde doit lire en premier.", + "tutorial_intro.end": "Cliquez sur \"Suivant\" pour entrer dans {channel}. C'est le premier canal que les membres voient quand ils s'inscrivent. Utilisez-le pour poster des messages que tout le monde doit lire en premier.", "tutorial_intro.invite": "Inviter des membres", "tutorial_intro.next": "Suivant", "tutorial_intro.screenOne": "<h3>Bienvenue sur :</h3><h1>Mattermost</h1><p>Toute la communication de votre Γ©quipe Γ un seul endroit, Your team communication all in one place, instantanΓ©ment consultable et disponible partout.</p><p>Gardez le lien avec votre Γ©quipe pour accomplir les tΓ’ches les plus importantes.</p>", @@ -1247,7 +1247,6 @@ "user.settings.general.close": "Quitter", "user.settings.general.confirmEmail": "Courriel de confirmation", "user.settings.general.email": "Adresse Γ©lectronique", - "user.settings.general.emailCantUpdate": "La connexion s'effectue par GitLab. L'adresse Γ©lectronique ne peut Γͺtre modifiΓ©e.", "user.settings.general.emailHelp1": "L'adresse Γ©lectronique est utilisΓ© pour la connexion, les notifications et la rΓ©initialisation du mot de passe. Votre adresse Γ©lectronique doit Γͺtre validΓ© si vous le changez.", "user.settings.general.emailHelp2": "Les courriels sont dΓ©sactivΓ©s par votre administrateur systΓ¨me. Aucune notification ne peut Γͺtre envoyΓ©e.", "user.settings.general.emailHelp3": "L'adresse Γ©lectronique est utilisΓ©e pour la connexion, les notifications et la rΓ©initialisation du mot de passe.", @@ -1258,7 +1257,7 @@ "user.settings.general.imageTooLarge": "Impossible de mettre Γ jour votre photo de profil. Le fichier est trop grand.", "user.settings.general.imageUpdated": "Image mise Γ jour le {date}", "user.settings.general.lastName": "Nom", - "user.settings.general.loginGitlab": "Connexion avec GitLab", + "user.settings.general.loginGitlab": "Connexion avec GitLab ({email})", "user.settings.general.newAddress": "Nouvelle adresse : {email}<br />VΓ©rifiez votre messagerie pour valider votre adresse Γ©lectronique.", "user.settings.general.nickname": "Pseudo", "user.settings.general.nicknameExtra": "Vous pouvez utiliser un pseudo Γ la place de vos prΓ©nom, nom et nom d'utilisateur. Ceci est pratique lorsque deux personnes de votre Γ©quipe ont des noms proches.", @@ -1274,27 +1273,6 @@ "user.settings.general.usernameRestrictions": "Les noms d'utilisateurs doivent commencer par une lettre et contenir entre {min} et {max} caractΓ¨res composΓ©s de chiffres, lettres minuscules et des symboles '.', '-' et '_'", "user.settings.general.validEmail": "Veuillez entrer une adresse Γ©lectronique valide", "user.settings.general.validImage": "Seules les images JPG ou PNG sont autorisΓ©es pour les photos de profil", - "user.settings.hooks_in.add": "Ajouter", - "user.settings.hooks_in.addTitle": "Ajouter un webhook entrant", - "user.settings.hooks_in.channel": "Canal\u00a0: ", - "user.settings.hooks_in.description": "Crééez des URLs de webhooks pour des intΓ©grations externes. Veuillez consulter <a href=\"http://docs.mattermost.com/developer/webhooks-incoming.html\" target=\"_blank\">la documentation sur les webhooks entrants</a> pour en savoir plus. Examinez tous les webhooks entrants configurΓ©s pour cette Γ©quipe ci-dessous.", - "user.settings.hooks_in.existing": "Webhooks entrants", - "user.settings.hooks_in.none": "Aucun", - "user.settings.hooks_out.add": "Ajouter", - "user.settings.hooks_out.addDescription": "Crééez des webhooks pour envoyer les Γ©vΓ¨nements de nouveaux messages vers des intΓ©grations externes. Veuillez consulter <a href=\"http://docs.mattermost.com/developer/webhooks-outgoing.html\" target=\"_blank\">la documentation sur les webhooks sortants</a> pour en savoir plus. Examinez tous les webhooks sortants configurΓ©s pour cette Γ©quipe ci-dessous.", - "user.settings.hooks_out.addTitle": "Ajouter un webhook sortant", - "user.settings.hooks_out.callback": "URLs de callback :", - "user.settings.hooks_out.callbackDesc": "URLs sΓ©parΓ©s par un saut de ligne qui recevront l'Γ©vΓ©nement HTTP POST", - "user.settings.hooks_out.callbackHolder": "Chaque URL doit commencer par http:// ou https://", - "user.settings.hooks_out.channel": "Canal\u00a0: ", - "user.settings.hooks_out.comma": "Liste de mots dΓ©clencheurs sΓ©parΓ©s par une virgule", - "user.settings.hooks_out.existing": "Webhooks sortants", - "user.settings.hooks_out.none": "Aucun", - "user.settings.hooks_out.only": "Seuls les canaux publics peuvent Γͺtre utilisΓ©s", - "user.settings.hooks_out.optional": "Facultatif si un canal est sΓ©lectionnΓ©", - "user.settings.hooks_out.regen": "RΓ©initialiser le jeton", - "user.settings.hooks_out.select": "--- Choisissez un canal ---", - "user.settings.hooks_out.trigger": "Mots de dΓ©clenchement :", "user.settings.import_theme.cancel": "Annuler", "user.settings.import_theme.importBody": "Pour importer un thΓ¨me, rendez-vous sur une Slack team et cliquez sur \"Preferences -> Sidebar Theme\". Ouvrez la fenΓͺtre de personnalisation, copiez les couleurs du thΓ¨mes et collez-les ici :", "user.settings.import_theme.importHeader": "Importer un thΓ¨me Slack", @@ -1302,10 +1280,6 @@ "user.settings.import_theme.submitError": "Format invalide, veuillez rΓ©essayer de copier-coller.", "user.settings.integrations.commands": "Commandes slash", "user.settings.integrations.commandsDescription": "GΓ©rez vos commandes slash", - "user.settings.integrations.incomingWebhooks": "Webhooks entrants", - "user.settings.integrations.incomingWebhooksDescription": "GΓ©rer les webhooks entrants", - "user.settings.integrations.outWebhooks": "Webhooks sortants", - "user.settings.integrations.outWebhooksDescription": "GΓ©rer les webhooks sortants", "user.settings.integrations.title": "ParamΓ¨tres d'intΓ©gration", "user.settings.languages.change": "Changer la langue de l'interface", "user.settings.modal.advanced": "Options avancΓ©es", @@ -1374,4 +1348,4 @@ "web.footer.terms": "Termes", "web.header.back": "PrΓ©cΓ©dent", "web.root.singup_info": "Toute la communication de votre Γ©quipe Γ un endroit, accessible de partout" -}
\ No newline at end of file +} diff --git a/webapp/i18n/pt.json b/webapp/i18n/pt.json index 0b06b77af..7525306e6 100644 --- a/webapp/i18n/pt.json +++ b/webapp/i18n/pt.json @@ -22,6 +22,28 @@ "activity_log_modal.android": "Android", "activity_log_modal.androidNativeApp": "App Nativo Android", "activity_log_modal.iphoneNativeApp": "App Nativo para iPhone", + "add_incoming_webhook.cancel": "Cancelar", + "add_incoming_webhook.channel": "Canal", + "add_incoming_webhook.channelRequired": "Um canal vΓ‘lido Γ© necessΓ‘rio", + "add_incoming_webhook.description": "Descrição", + "add_incoming_webhook.header": "Adicionar Webhooks Entrada", + "add_incoming_webhook.name": "Nome", + "add_incoming_webhook.save": "Salvar", + "add_integration.header": "Adicionar Integração", + "add_integration.incomingWebhook.title": "Webhooks Entrada", + "add_integration.incomingWebhook.description": "Criar URLs webhook para usar em integraçáes externas", + "add_integration.outgoingWebhook.title": "Webhooks SaΓda", + "add_integration.outgoingWebhook.description": "Criar webhook para enviar novos eventos de mensagens para uma integração externa.", + "add_outgoing_webhook.callbackUrls": "URLs Callback (Uma Por Linha)", + "add_outgoing_webhook.callbackUrlsRequired": "Uma ou mais URLs callback sΓ£o necessΓ‘rias", + "add_outgoing_webhook.cancel": "Cancelar", + "add_outgoing_webhook.channel": "Canal", + "add_outgoing_webhook.description": "Descrição", + "add_outgoing_webhook.header": "Adicionar Webhooks SaΓda", + "add_outgoing_webhook.name": "Nome", + "add_outgoing_webhook.save": "Salvar", + "add_outgoing_webhook.triggerWOrds": "Palavras Gatilho (Uma Por Linha)", + "add_outgoing_webhook.triggerWordsOrChannelRequired": "Um canal vΓ‘lido ou uma lista de palavras gatilho Γ© necessΓ‘rio", "admin.audits.reload": "Recarregar", "admin.audits.title": "Atividade de UsuΓ‘rio", "admin.compliance.directoryDescription": "DiretΓ³rio o qual os relatΓ³rios compliance sΓ£o gravados, Se estiver em branco, serΓ‘ usado ./data/.", @@ -230,15 +252,9 @@ "admin.ldap.usernameAttrTitle": "Atributo do UsuΓ‘rio:", "admin.licence.keyMigration": "Se vocΓͺ estiver migrando seu servidor vocΓͺ pode precisar remover sua chave da licenΓ§a deste servidor a pedido para instala-la em um novo servidor. Para iniciar, <a href=\"http://mattermost.com\" target=\"_blank\">desativar todos os recursos Enterprise Edition deste servidor</a>. Isto irΓ‘ habilitar para remover a chave da licenΓ§a e fazer downgrade deste servidor Enterprise Edition para Team Edition.", "admin.license.chooseFile": "Escolha um Arquivo", - "admin.license.edition": "Edição: ", - "admin.license.enterpriseEdition": "Mattermost Enterprise Edition. Desenvolvido para escala empresarial de comunicação.", - "admin.license.enterpriseType": "<div><p>Esta versΓ£o compilada da plataforma Mattermost Γ© fornecida sob a <a href=\"http://mattermost.com\" target=\"_blank\">licenΓ§a comercial</a> para Mattermost, Inc. com base em seu nΓvel de subscrição e estΓ‘ sujeito a <a href=\"{terms}\" target=\"_blank\">Termos de ServiΓ§o.</a></p><p>Os detalhes de sua assinatura, sΓ£o como segue:</p>Nome: {name}<br />Nome da Empresa ou organização: {company}<br/>NΓΊmero de usuΓ‘rios: {users}<br/>LicenΓ§a emitida: {issued}<br/>Data de InΓcio da licenΓ§a: {start}<br/>Data de expiração da licenΓ§a: {expires}<br/>LDAP: {ldap}<br/></div>", - "admin.license.key": "Chave da LicenΓ§a: ", "admin.license.keyRemove": "Remover a LicenΓ§a Enterprise e fazer Downgrade do Servidor", "admin.license.noFile": "Nenhum arquivo enviado", "admin.license.removing": "Removendo a LicenΓ§a...", - "admin.license.teamEdition": "Mattermost Team Edition. Desenvolvido para equipes de 5 a 50 usuΓ‘rios.", - "admin.license.teamType": "<span><p>Esta versΓ£o compilada da plataforma Mattermost Γ© oferecido sob uma licenΓ§a MIT.</p><p>Ver MIT-COMPILED-LICENSE.txt no raiz do diretΓ³rio de instalação para obter detalhes. Ver NOTICES.txt para obter informaçáes sobre o software open source usados neste sistema.</p></span>", "admin.license.title": "Edição e LicenΓ§a", "admin.license.type": "LicenΓ§a: ", "admin.license.upload": "Enviar", @@ -312,6 +328,8 @@ "admin.select_team.close": "Fechar", "admin.select_team.select": "Selecionar", "admin.select_team.selectTeam": "Selecione Equipe", + "admin.service.mfaTitle": "Ativar Autenticação Multi-Fator:", + "admin.service.mfaDesc": "Quando verdadeiro, vai ser dada a opção do usuΓ‘rio adicionar autenticação multi-fator em sua conta. Eles irΓ£o precisar de um smartphone e um app autenticador como o Google Authenticator.", "admin.service.attemptDescription": "Tentativas de login permitidas antes que do usuΓ‘rio ser bloqueado e necessΓ‘rio redefinir a senha por e-mail.", "admin.service.attemptExample": "Ex \"10\"", "admin.service.attemptTitle": "MΓ‘xima Tentativas de Login:", @@ -416,6 +434,8 @@ "admin.support.emailTitle": "E-mail de suporte:", "admin.support.helpDesc": "Link para documentação de ajuda para o site da equipe no menu principal. Normalmente nΓ£o Γ© alterado ao menos se sua empresa escolha criar uma documentação customizada.", "admin.support.helpTitle": "Link de ajuda:", + "admin.support.noteDescription": "Se links para um site externo, URLs devem comeΓ§ar com http:// ou https://.", + "admin.support.noteTitle": "Nota:", "admin.support.privacyDesc": "Link para PolΓtica de Privacidade para os usuΓ‘rios no desktop ou mΓ³vel. Deixando este espaΓ§o em branco irΓ‘ esconder a opção de exibir um aviso.", "admin.support.privacyTitle": "Link da PolΓtica de Privacidade:", "admin.support.problemDesc": "Link para a documentação de ajuda do site no menu principal. Por padrΓ£o este aponta para um fΓ³rum peer-to-peer de solução de problemas onde os usuΓ‘rios podem pesquisar, encontrar e pedir ajuda com problemas tΓ©cnicos.", @@ -425,8 +445,6 @@ "admin.support.termsDesc": "Link para os Termos de ServiΓ§o para os usuΓ‘rios no desktop ou mΓ³vel. Deixando este espaΓ§o em branco irΓ‘ esconder a opção de exibir um aviso.", "admin.support.termsTitle": "Link Termos do ServiΓ§o:", "admin.support.title": "Configuraçáes jurΓdico e apoio", - "admin.support.noteTitle": "Note:", - "admin.support.noteDescription": "If linking to an external site, URLs should begin with http:// or https://.", "admin.system_analytics.activeUsers": "UsuΓ‘rios Ativos com Postagens", "admin.system_analytics.title": "o Sistema", "admin.system_analytics.totalPosts": "Total Posts", @@ -556,6 +574,12 @@ "authorize.app": "O app <strong>{appName}</strong> gostaria de ter a capacidade de acessar e modificar suas informaçáes bΓ‘sicas.", "authorize.deny": "Negar", "authorize.title": "Um aplicativo gostaria de conectar na sua conta {teamName}", + "backstage_navbar.backToMattermost": "Voltar para {siteName}", + "backstage_sidebar.integrations": "Integraçáes", + "backstage_sidebar.integrations.installed": "Integraçáes Instaladas", + "backstage_sidebar.integrations.add": "Adicionar Integração", + "backstage_sidebar.integrations.add.incomingWebhook": "Webhooks Entrada", + "backstage_sidebar.integrations.add.outgoingWebhook": "Webhooks SaΓda", "center_panel.recent": "Clique aqui para pular para mensagens recentes. ", "chanel_header.addMembers": "Adicionar Membros", "change_url.close": "Fechar", @@ -632,6 +656,7 @@ "channel_notifications.preferences": "PreferΓͺncias de Notificação para ", "channel_notifications.sendDesktop": "Enviar notificaçáes de desktop", "channel_notifications.unreadInfo": "O nome do canal fica em negrito na barra lateral quando houver mensagens nΓ£o lidas. Selecionando \"Apenas mençáes\" o canal vai ficar em negrito apenas quando vocΓͺ for mencionado.", + "channel_select.placeholder": "--- Selecione um canal ---", "choose_auth_page.emailCreate": "Criar uma nova equipe com endereΓ§o de email", "choose_auth_page.find": "Encontrar minhas equipes", "choose_auth_page.gitlabCreate": "Criar uma equipe com uma conta GitLab", @@ -639,6 +664,18 @@ "choose_auth_page.ldapCreate": "Criar uma nova equipe com uma conta LDAP", "choose_auth_page.noSignup": "Nenhum mΓ©todo de inscrição configurado, por favor contate seu administrador do sistema.", "claim.account.noEmail": "Nenhum email especΓficado", + "claim.email_to_ldap.enterLdapPwd": "Entre o ID e a senha para sua conta LDAP", + "claim.email_to_ldap.enterPwd": "Entre a senha para o sua conta com email {team} {site}", + "claim.email_to_ldap.ldapId": "LDAP ID", + "claim.email_to_ldap.ldapIdError": "Por favor digite seu ID LDAP.", + "claim.email_to_ldap.ldapPasswordError": "Por favor digite a sua senha LDAP.", + "claim.email_to_ldap.ldapPwd": "Senha LDAP", + "claim.email_to_ldap.pwd": "Senha", + "claim.email_to_ldap.pwdError": "Por favor digite a sua senha.", + "claim.email_to_ldap.ssoNote": "VocΓͺ precisa jΓ‘ ter uma conta LDAP vΓ‘lida", + "claim.email_to_ldap.ssoType": "Ao retirar a sua conta, vocΓͺ sΓ³ vai ser capaz de logar com LDAP", + "claim.email_to_ldap.switchTo": "Trocar a conta para LDAP", + "claim.email_to_ldap.title": "Trocar E-mail/Senha da Conta para LDAP", "claim.email_to_oauth.enterPwd": "Entre a senha para o sua conta {team} {site}", "claim.email_to_oauth.pwd": "Senha", "claim.email_to_oauth.pwdError": "Por favor digite a sua senha.", @@ -646,14 +683,25 @@ "claim.email_to_oauth.ssoType": "Ao retirar a sua conta, vocΓͺ sΓ³ vai ser capaz de logar com SSO {type}", "claim.email_to_oauth.switchTo": "Trocar a conta para {uiType}", "claim.email_to_oauth.title": "Trocar E-mail/Senha da Conta para {uiType}", - "claim.oauth_to_email.confirm": "Confirmar senha", + "claim.ldap_to_email.confirm": "Confirmar senha", + "claim.ldap_to_email.email": "VocΓͺ vai usar o email {email} para logar", + "claim.ldap_to_email.enterLdapPwd": "Entre a sua senha LDAP para o sua conta {team} {site}", + "claim.ldap_to_email.enterPwd": "Entre a nova senha para o sua conta com email.", + "claim.ldap_to_email.ldapPasswordError": "Por favor digite a sua senha LDAP.", + "claim.ldap_to_email.ldapPwd": "Senha LDAP", + "claim.ldap_to_email.pwd": "Senha", + "claim.ldap_to_email.pwdError": "Por favor digite a sua senha.", + "claim.ldap_to_email.pwdNotMatch": "As senha nΓ£o correspondem.", + "claim.ldap_to_email.ssoType": "ApΓ³s a alteração do tipo de conta, vocΓͺ sΓ³ vai ser capaz de logar com seu e-mail e senha.", + "claim.ldap_to_email.switchTo": "Trocar a conta para e-mail/senha", + "claim.ldap_to_email.title": "Trocar a conta LDAP para E-mail/Senha", + "claim.oauth_to_email.confirm": "Confirmar Senha", "claim.oauth_to_email.description": "ApΓ³s a alteração do tipo de conta, vocΓͺ sΓ³ vai ser capaz de logar com seu e-mail e senha.", "claim.oauth_to_email.enterPwd": "Por favor entre uma senha.", "claim.oauth_to_email.newPwd": "Nova Senha", "claim.oauth_to_email.pwdNotMatch": "As senha nΓ£o correspondem.", "claim.oauth_to_email.switchTo": "Trocar {type} para email e senha", "claim.oauth_to_email.title": "Trocar Conta {type} para E-mail", - "claim.oauth_to_email.newPwd": "Entre a nova senha para o sua conta {team} {site}", "confirm_modal.cancel": "Cancelar", "create_comment.addComment": "Adicionar um comentΓ‘rio...", "create_comment.comment": "Adicionar ComentΓ‘rio", @@ -751,6 +799,16 @@ "get_team_invite_link_modal.help": "Enviar o link abaixo para sua equipe de trabalho para que eles se inscrevam no site da sua equipe. O Link de Convite de Equipe como ele nΓ£o muda pode ser compartilhado com vΓ‘rias pessoas ao menos que seja re-gerado em Configuraçáes de Equipe pelo Administrador de Equipe.", "get_team_invite_link_modal.helpDisabled": "Criação de usuΓ‘rios estΓ‘ desabilitada para sua equipe. Por favor peΓ§a ao administrador de equipe por detalhes.", "get_team_invite_link_modal.title": "Link para Convite de Equipe", + "installed_integrations.add": "Adicionar Integração", + "installed_integrations.allFilter": "Todos", + "installed_integrations.delete": "Deletar", + "installed_integrations.header": "Integraçáes Instaladas", + "installed_integrations.incomingWebhooksFilter": "Webhooks Entrada ({count})", + "installed_integrations.incomingWebhookType": "(Webhooks Entrada)", + "installed_integrations.outgoingWebhooksFilter": "Webhooks SaΓda ({count})", + "installed_integrations.outgoingWebhookType": "(Webhooks SaΓda)", + "installed_integrations.regenToken": "Regen Token", + "installed_integrations.search": "Pesquisar Integraçáes", "intro_messages.DM": "Este Γ© o inΓcio do seu histΓ³rico de mensagens diretas com {teammate}.<br />Mensagens diretas e arquivos compartilhados aqui nΓ£o sΓ£o mostrados para pessoas de fora desta Γ‘rea.", "intro_messages.anyMember": " Qualquer membro pode participar e ler este canal.", "intro_messages.beginning": "InΓcio do {name}", @@ -801,6 +859,10 @@ "login.session_expired": " Sua sessΓ£o expirou. Por favor faΓ§a login novamente.", "login.signTo": "Login em:", "login.verified": " Email Verificado", + "login_mfa.token": "Token MFA", + "login_mfa.enterToken": "Para completar o login em processo, por favor entre um token do seu autenticador no smartphone", + "login_mfa.submit": "Enviar", + "login_mfa.tokenReq": "Por favor entre um token MFA", "login_email.badTeam": "Nome ruim de equipe", "login_email.email": "E-mail", "login_email.emailReq": "Um email Γ© necessΓ‘rio", @@ -856,6 +918,7 @@ "navbar_dropdown.console": "Console do Sistema", "navbar_dropdown.create": "Criar uma Nova Equipe", "navbar_dropdown.help": "Ajuda", + "navbar_dropdown.integrations": "Integraçáes", "navbar_dropdown.inviteMember": "Convidar Membros da Equipe", "navbar_dropdown.logout": "Logout", "navbar_dropdown.manageMembers": "Gerenciar Membros", @@ -968,7 +1031,7 @@ "sidebar.pg": "Grupos Privados", "sidebar.removeList": "Remover da lista", "sidebar.tutorialScreen1": "<h4>Canais</h4><p><strong>Canais</strong> organizar conversas em diferentes tΓ³picos. Eles estΓ£o abertos a todos em sua equipe. Para enviar comunicaçáes privadas utilize <strong>Mensagens Diretas</strong> para uma ΓΊnica pessoa ou <strong>Grupos Privados</strong> para vΓ‘rias pessoas.</p>", - "sidebar.tutorialScreen2": "<h4>Canais \"Town Square\" e \"Off-Topic\"</h4><p>Aqui estΓ£o dois canais pΓΊblicos para comeΓ§ar:</p><p><strong>Town Square</strong> Γ© um lugar comunicação de toda equipe. Todo mundo em sua equipe Γ© um membro deste canal.</p><p><strong>Off-Topic</strong> Γ© um lugar para diversΓ£o e humor fora dos canais relacionados com o trabalho. VocΓͺ e sua equipe podem decidir qual outros canais serΓ£o criados.</p>", + "sidebar.tutorialScreen2": "<h4>Canais \"{townsquare}\" e \"{offtopic}\"</h4><p>Aqui estΓ£o dois canais pΓΊblicos para comeΓ§ar:</p><p><strong>{townsquare}</strong> Γ© um lugar comunicação de toda equipe. Todo mundo em sua equipe Γ© um membro deste canal.</p><p><strong>{offtopic}</strong> Γ© um lugar para diversΓ£o e humor fora dos canais relacionados com o trabalho. VocΓͺ e sua equipe podem decidir qual outros canais serΓ£o criados.</p>", "sidebar.tutorialScreen3": "<h4>Criando e participando de Canais</h4><p>Clique em <strong>\"Mais...\"</strong> para criar um novo canal ou participar de um jΓ‘ existente.</p><p>VocΓͺ tambΓ©m pode criar um novo canal ou grupo privado ao clicar em <strong>no sΓmbolo \"+\"</strong> ao lado do canal ou grupo privado no cabeΓ§alho.</p>", "sidebar.unreadAbove": "Post(s) nΓ£o lidos abaixo", "sidebar.unreadBelow": "Post(s) nΓ£o lidos abaixo", @@ -1116,7 +1179,7 @@ "textbox.quote": ">citar", "textbox.strike": "tachado", "tutorial_intro.allSet": "EstΓ‘ tudo pronto", - "tutorial_intro.end": "Clique em βPrΓ³ximoβ para entrar Town Square. Este Γ© o primeiro canal que sua equipe de trabalho vΓͺ quando eles se inscrevem. Use para postar atualizaçáes que todos precisam saber.", + "tutorial_intro.end": "Clique em βPrΓ³ximoβ para entrar {channel}. Este Γ© o primeiro canal que sua equipe de trabalho vΓͺ quando eles se inscrevem. Use para postar atualizaçáes que todos precisam saber.", "tutorial_intro.invite": "Convidar pessoas para equipe", "tutorial_intro.next": "PrΓ³ximo", "tutorial_intro.screenOne": "<h3>Bem vindo ao:</h3><h1>Mattermost</h1><p>Sua equipe de comunicação em um sΓ³ lugar, pesquisas instantΓ’neas disponΓvel em qualquer lugar</p><p>Mantenha sua equipe conectada para ajudΓ‘-los a conseguir o que mais importa.</p>", @@ -1221,6 +1284,7 @@ "user.settings.display.theme.customTheme": "Tema Customizado", "user.settings.display.theme.describe": "Abrir para gerenciar seu tema", "user.settings.display.theme.import": "Importar tema de cores do Slack", + "user.settings.display.theme.otherThemes": "Veja outros temas", "user.settings.display.theme.themeColors": "Tema de Cores", "user.settings.display.theme.title": "Tema", "user.settings.display.title": "Configuraçáes de Exibição", @@ -1229,18 +1293,22 @@ "user.settings.general.close": "Fechar", "user.settings.general.confirmEmail": "Confirmar o email", "user.settings.general.email": "E-mail", - "user.settings.general.emailCantUpdate": "Login ocorreu atravΓ©s do GitLab. Email nΓ£o pode ser atualizado.", + "user.settings.general.emailGitlabCantUpdate": "Login ocorre atravΓ©s do GitLab. Email nΓ£o pode ser atualizado. EndereΓ§o de email utilizado para notificaçáes Γ© {email}.", + "user.settings.general.emailLdapCantUpdate": "Login ocorre atravΓ©s de LDAP. Email nΓ£o pode ser atualizado. EndereΓ§o de email utilizado para notificaçáes Γ© {email}.", "user.settings.general.emailHelp1": "Email Γ© usado para login, notificaçáes, e redefinição de senha. Requer verificação de email se alterado.", "user.settings.general.emailHelp2": "Email foi desativado pelo seu administrador de sistema. Nenhuma notificação por email serΓ‘ enviada atΓ© isto ser habilitado.", "user.settings.general.emailHelp3": "Email Γ© usado para login, notificaçáes e redefinição de senha.", "user.settings.general.emailHelp4": "Uma verificação por email foi enviada para {email}.", "user.settings.general.emailMatch": "Os novos emails que vocΓͺ inseriu nΓ£o correspondem.", + "user.settings.general.emptyName": "Clique 'Editar' para adicionar seu nome completo", + "user.settings.general.emptyNickname": "Clique 'Editar' para adicionar um apelido", "user.settings.general.firstName": "Primeiro nome", "user.settings.general.fullName": "Nome Completo", "user.settings.general.imageTooLarge": "NΓ£o Γ© possΓvel fazer upload da imagem de perfil. O arquivo Γ© muito grande.", "user.settings.general.imageUpdated": "Imagem ΓΊltima atualização {date}", "user.settings.general.lastName": "Γltimo Nome", - "user.settings.general.loginGitlab": "Login feito atravΓ©s do GitLab", + "user.settings.general.loginGitlab": "Login feito atravΓ©s do GitLab ({email})", + "user.settings.general.loginLdap": "Login feito atravΓ©s de LDAP ({email})", "user.settings.general.newAddress": "Novo EndereΓ§o: {email}<br />Verifique seu email para checar o endereΓ§o acima.", "user.settings.general.nickname": "Apelido", "user.settings.general.nicknameExtra": "Use Apelidos para um nome vocΓͺ pode ser chamado assim, isso Γ© diferente de seu primeiro nome e nome de usuΓ‘rio. Este Γ© mais frequentemente usado quando duas ou mais pessoas tΓͺm nomes semelhantes de usuΓ‘rio.", @@ -1256,27 +1324,6 @@ "user.settings.general.usernameRestrictions": "O nome de usuΓ‘rio precisa comeΓ§ar com uma letra, e conter entre {min} e {max} caracteres minΓΊsculos contendo nΓΊmeros, letras, e os sΓmbolos '.', '-' e '_'.", "user.settings.general.validEmail": "Por favor entre um endereΓ§o de e-mail vΓ‘lido", "user.settings.general.validImage": "Somente imagens em JPG ou PNG podem ser usadas como imagem do perfil", - "user.settings.hooks_in.add": "Adicionar", - "user.settings.hooks_in.addTitle": "Adicionar um novo webhook entrada", - "user.settings.hooks_in.channel": "Canal: ", - "user.settings.hooks_in.description": "Criar URLs webhook para usar em integraçáes externas. Por favor veja <a href=\"http://docs.mattermost.com/developer/webhooks-incoming.html\" target=\"_blank\">documentação webhook entrada</a> para saber mais. Ver todos os webhooks de entrada configurados nesta equipe abaixo.", - "user.settings.hooks_in.existing": "Webhooks de entrada existentes", - "user.settings.hooks_in.none": "Nenhum", - "user.settings.hooks_out.add": "Adicionar", - "user.settings.hooks_out.addDescription": "Criar webhooks para enviar novos mensagens de eventos para uma integração externa. Por favor veja <a href=\"http://docs.mattermost.com/developer/webhooks-outgoing.html\" target=\"_blank\">documentação webhook saΓda</a> para saber mais. Ver todos os webhooks de saΓda desta equipe configurados abaixo.", - "user.settings.hooks_out.addTitle": "Adicionar um novo webhook saΓda", - "user.settings.hooks_out.callback": "Callback URLs: ", - "user.settings.hooks_out.callbackDesc": "Nova linha separada de URLs que receberΓ‘ o evento HTTP POST", - "user.settings.hooks_out.callbackHolder": "Cada URL deve comeΓ§ar com http:// ou https://", - "user.settings.hooks_out.channel": "Canal: ", - "user.settings.hooks_out.comma": "Palavras separadas por virgula para gatilho em", - "user.settings.hooks_out.existing": "Webhooks de saΓda existentes", - "user.settings.hooks_out.none": "Nenhum", - "user.settings.hooks_out.only": "Apenas canais pΓΊblicos pode ser usado", - "user.settings.hooks_out.optional": "Opcional se o canal selecionado", - "user.settings.hooks_out.regen": "Re-Gerar Token", - "user.settings.hooks_out.select": "--- Selecione um canal ---", - "user.settings.hooks_out.trigger": "Palavras de Gatilho: ", "user.settings.import_theme.cancel": "Cancelar", "user.settings.import_theme.importBody": "Para importar um tema, vΓ‘ para uma equipe no Slack e olhe para βPreferences -> Sidebar Themeβ. Abra a opção de tema customizado, copie os valores das cores do tema e cole eles aqui:", "user.settings.import_theme.importHeader": "Importar Tema Slack", @@ -1284,10 +1331,6 @@ "user.settings.import_theme.submitError": "Formato invΓ‘lido, por favor tente copiar e colar novamente.", "user.settings.integrations.commands": "Comandos Slash", "user.settings.integrations.commandsDescription": "Gerenciar seus comandos slash", - "user.settings.integrations.incomingWebhooks": "Webhooks Entrada", - "user.settings.integrations.incomingWebhooksDescription": "Gerencie seus webhooks entrada", - "user.settings.integrations.outWebhooks": "Webhooks SaΓda", - "user.settings.integrations.outWebhooksDescription": "Gerencie seus webhooks saΓda", "user.settings.integrations.title": "Configuração de Integração", "user.settings.languages.change": "Alterar o idioma da interface", "user.settings.modal.advanced": "AvanΓ§ado", @@ -1330,17 +1373,22 @@ "user.settings.security.emailPwd": "Email e Senha", "user.settings.security.gitlab": "GitLab SSO", "user.settings.security.lastUpdated": "Γltima atualização {date} {time}", + "user.settings.security.loginGitlab": "Login feito atravΓ©s do GitLab", + "user.settings.security.loginLdap": "Login feito atravΓ©s de LDAP", "user.settings.security.logoutActiveSessions": "Ver e fazer Logout das SessΓ΅es Ativas", "user.settings.security.method": "MΓ©todo de Login", "user.settings.security.newPassword": "Nova Senha", "user.settings.security.oneSignin": "VocΓͺ pode ter somente um mΓ©todo de login por vez. Trocando o mΓ©todo de login serΓ‘ enviado um email de notificação se vocΓͺ alterar com sucesso.", "user.settings.security.password": "Senha", + "user.settings.security.passwordGitlabCantUpdate": "Login ocorreu atravΓ©s do GitLab. Senha nΓ£o pode ser atualizada.", + "user.settings.security.passwordLdapCantUpdate": "Login ocorreu atravΓ©s de LDAP. Senha nΓ£o pode ser atualizada.", "user.settings.security.passwordLengthError": "Novas senhas precisam ter pelo menos {chars} characters", "user.settings.security.passwordMatchError": "As novas senhas que vocΓͺ inseriu nΓ£o correspondem", "user.settings.security.retypePassword": "Digite Novamente a nova Senha", "user.settings.security.switchEmail": "Trocar para usar email e senha", "user.settings.security.switchGitlab": "Trocar para usar GitLab SSO", "user.settings.security.switchGoogle": "Trocar para usar Google SSO", + "user.settings.security.switchLda": "Trocar para usar LDAP", "user.settings.security.title": "Configuraçáes de SeguranΓ§a", "user.settings.security.viewHistory": "Ver HistΓ³rico de Acesso", "user_list.notFound": "Nenhum usuΓ‘rio encontrado :(", diff --git a/webapp/images/emoji/1f1e8-1f1e6.png b/webapp/images/emoji/1f1e8-1f1e6.png Binary files differnew file mode 100644 index 000000000..57f487c22 --- /dev/null +++ b/webapp/images/emoji/1f1e8-1f1e6.png diff --git a/webapp/images/emoji/1f1f5-1f1f0.png b/webapp/images/emoji/1f1f5-1f1f0.png Binary files differnew file mode 100644 index 000000000..17c4f6db5 --- /dev/null +++ b/webapp/images/emoji/1f1f5-1f1f0.png diff --git a/webapp/images/emoji/1f1ff-1e1e6.png b/webapp/images/emoji/1f1ff-1e1e6.png Binary files differnew file mode 100644 index 000000000..8909fe82a --- /dev/null +++ b/webapp/images/emoji/1f1ff-1e1e6.png diff --git a/webapp/images/emoji/1f641.png b/webapp/images/emoji/1f641.png Binary files differnew file mode 100644 index 000000000..7041b0804 --- /dev/null +++ b/webapp/images/emoji/1f641.png diff --git a/webapp/images/emoji/1f642.png b/webapp/images/emoji/1f642.png Binary files differnew file mode 100644 index 000000000..abd534797 --- /dev/null +++ b/webapp/images/emoji/1f642.png diff --git a/webapp/images/emoji/1f643.png b/webapp/images/emoji/1f643.png Binary files differnew file mode 100644 index 000000000..3cb9f962f --- /dev/null +++ b/webapp/images/emoji/1f643.png diff --git a/webapp/package.json b/webapp/package.json index 6f50962a4..01674ba1c 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -10,6 +10,7 @@ "compass-mixins": "0.12.7", "fastclick": "1.0.6", "flux": "2.1.1", + "font-awesome": "4.5.0", "highlight.js": "9.2.0", "intl": "1.1.0", "jasny-bootstrap": "3.1.3", @@ -56,6 +57,7 @@ "scripts": { "check": "eslint --ext \".jsx\" --ignore-pattern node_modules --quiet .", "build": "webpack", - "run": "webpack --progress --watch" + "run": "webpack --progress --watch", + "run-fullmap": "webpack --progress --watch" } } diff --git a/webapp/root.jsx b/webapp/root.jsx index 2318c0682..da5980c33 100644 --- a/webapp/root.jsx +++ b/webapp/root.jsx @@ -4,23 +4,21 @@ import $ from 'jquery'; require('perfect-scrollbar/jquery')($); -import 'bootstrap/dist/css/bootstrap.css'; -import 'jasny-bootstrap/dist/css/jasny-bootstrap.css'; import 'bootstrap-colorpicker/dist/css/bootstrap-colorpicker.css'; import 'google-fonts/google-fonts.css'; import 'sass/styles.scss'; import React from 'react'; import ReactDOM from 'react-dom'; -import {Router, Route, IndexRoute, IndexRedirect, browserHistory} from 'react-router'; +import {Router, Route, IndexRoute, IndexRedirect, Redirect, browserHistory} from 'react-router'; import Root from 'components/root.jsx'; -import Login from 'components/login.jsx'; import LoggedIn from 'components/logged_in.jsx'; import NotLoggedIn from 'components/not_logged_in.jsx'; import NeedsTeam from 'components/needs_team.jsx'; import PasswordResetSendLink from 'components/password_reset_send_link.jsx'; import PasswordResetForm from 'components/password_reset_form.jsx'; import ChannelView from 'components/channel_view.jsx'; +import PermalinkView from 'components/permalink_view.jsx'; import Sidebar from 'components/sidebar.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; @@ -30,12 +28,21 @@ import BrowserStore from 'stores/browser_store.jsx'; import SignupTeam from 'components/signup_team.jsx'; import * as Client from 'utils/client.jsx'; import * as Websockets from 'action_creators/websocket_actions.jsx'; +import * as Utils from 'utils/utils.jsx'; import * as GlobalActions from 'action_creators/global_actions.jsx'; import SignupTeamConfirm from 'components/signup_team_confirm.jsx'; import SignupUserComplete from 'components/signup_user_complete.jsx'; import ShouldVerifyEmail from 'components/should_verify_email.jsx'; import DoVerifyEmail from 'components/do_verify_email.jsx'; import AdminConsole from 'components/admin_console/admin_controller.jsx'; +import TutorialView from 'components/tutorial/tutorial_view.jsx'; +import BackstageNavbar from 'components/backstage/backstage_navbar.jsx'; +import BackstageSidebar from 'components/backstage/backstage_sidebar.jsx'; +import InstalledIntegrations from 'components/backstage/installed_integrations.jsx'; +import AddIntegration from 'components/backstage/add_integration.jsx'; +import AddIncomingWebhook from 'components/backstage/add_incoming_webhook.jsx'; +import AddOutgoingWebhook from 'components/backstage/add_outgoing_webhook.jsx'; +import ErrorPage from 'components/error_page.jsx'; import SignupTeamComplete from 'components/signup_team_complete/components/signup_team_complete.jsx'; import WelcomePage from 'components/signup_team_complete/components/team_signup_welcome_page.jsx'; @@ -52,8 +59,17 @@ import OAuthToEmail from 'components/claim/components/oauth_to_email.jsx'; import LDAPToEmail from 'components/claim/components/ldap_to_email.jsx'; import EmailToLDAP from 'components/claim/components/email_to_ldap.jsx'; +import Login from 'components/login/login.jsx'; + import * as I18n from 'i18n/i18n.jsx'; +const notFoundParams = { + title: Utils.localizeMessage('error.not_found.title', 'Page not found'), + message: Utils.localizeMessage('error.not_found.message', 'The page you where trying to reach does not exist'), + link: '/', + linkmessage: Utils.localizeMessage('error.not_found.link_message', 'Back to Mattermost') +}; + // This is for anything that needs to be done for ALL react components. // This runs before we start to render anything. function preRenderSetup(callwhendone) { @@ -119,7 +135,7 @@ function preRenderSetup(callwhendone) { if (global.Intl) { afterIntl(); } else { - I18n.safarifix(afterIntl); + I18n.safariFix(afterIntl); } } @@ -135,20 +151,11 @@ function preLoggedIn(nextState, replace, callback) { const d2 = AsyncClient.getChannels(); - $.when(d1, d2).done(() => callback()); -} - -function onChannelChange(nextState) { - const channelName = nextState.params.channel; + ErrorStore.clearLastError(); - // Make sure we have all the channels - AsyncClient.getChannels(true); - - // Get our channel's ID - const channel = ChannelStore.getByName(channelName); - - // User clicked channel - GlobalActions.emitChannelClickEvent(channel); + $.when(d1, d2).done(() => { + callback(); + }); } function onRootEnter(nextState, replace, callback) { @@ -174,6 +181,26 @@ function onPermalinkEnter(nextState) { GlobalActions.emitPostFocusEvent(postId); } +function onChannelEnter(nextState) { + doChannelChange(nextState); +} + +function onChannelChange(prevState, nextState) { + if (prevState.params.channel !== nextState.params.channel) { + doChannelChange(nextState); + } +} + +function doChannelChange(state) { + let channel; + if (state.location.query.fakechannel) { + channel = JSON.parse(state.location.query.fakechannel); + } else { + channel = ChannelStore.getByName(state.params.channel); + } + GlobalActions.emitChannelClickEvent(channel); +} + function onLoggedOut(nextState) { const teamName = nextState.params.team; Client.logout( @@ -201,12 +228,17 @@ function renderRootComponent() { onEnter={onRootEnter} > <Route + path='error' + component={ErrorPage} + /> + <Route component={LoggedIn} onEnter={preLoggedIn} > <Route path=':team/channels/:channel' - onEnter={onChannelChange} + onEnter={onChannelEnter} + onChange={onChannelChange} components={{ sidebar: Sidebar, center: ChannelView @@ -217,23 +249,64 @@ function renderRootComponent() { onEnter={onPermalinkEnter} components={{ sidebar: Sidebar, - center: ChannelView + center: PermalinkView }} /> <Route - path=':team/logout' - onEnter={onLoggedOut} + path=':team/tutorial' components={{ - sidebar: null, - center: null + sidebar: Sidebar, + center: TutorialView }} /> <Route + path=':team/logout' + onEnter={onLoggedOut} + /> + <Route path='settings/integrations'> + <IndexRedirect to='installed'/> + <Route + path='installed' + components={{ + navbar: BackstageNavbar, + sidebar: BackstageSidebar, + center: InstalledIntegrations + }} + /> + <Route path='add'> + <IndexRoute + components={{ + navbar: BackstageNavbar, + sidebar: BackstageSidebar, + center: AddIntegration + }} + /> + <Route + path='incoming_webhook' + components={{ + navbar: BackstageNavbar, + sidebar: BackstageSidebar, + center: AddIncomingWebhook + }} + /> + <Route + path='outgoing_webhook' + components={{ + navbar: BackstageNavbar, + sidebar: BackstageSidebar, + center: AddOutgoingWebhook + }} + /> + </Route> + <Redirect + from='*' + to='/error' + query={notFoundParams} + /> + </Route> + <Route path='admin_console' - components={{ - sidebar: null, - center: AdminConsole - }} + component={AdminConsole} /> </Route> <Route component={NotLoggedIn}> @@ -325,6 +398,11 @@ function renderRootComponent() { component={LDAPToEmail} /> </Route> + <Redirect + from='*' + to='/error' + query={notFoundParams} + /> </Route> </Route> </Route> diff --git a/webapp/sass/components/_mentions.scss b/webapp/sass/components/_mentions.scss index 98ae7d320..4753b4e9a 100644 --- a/webapp/sass/components/_mentions.scss +++ b/webapp/sass/components/_mentions.scss @@ -47,7 +47,7 @@ } .mention__fullname { - color: $dark-gray; + @include opacity(.5); padding-left: 10px; } diff --git a/webapp/sass/components/_modal.scss b/webapp/sass/components/_modal.scss index 4e2049857..2348788f4 100644 --- a/webapp/sass/components/_modal.scss +++ b/webapp/sass/components/_modal.scss @@ -39,6 +39,38 @@ } } + .padding-top { + padding-top: 7px; + + &.x2 { + padding-top: 14px; + } + + &.x3 { + padding-top: 21px; + } + } + + .padding-bottom { + padding-bottom: 7px; + + &.x2 { + padding-bottom: 14px; + } + + &.x3 { + padding-bottom: 21px; + } + + .control-label { + font-weight: 600; + + &.text-left { + text-align: left; + } + } + } + .custom-textarea { border-color: $light-gray; color: inherit; diff --git a/webapp/sass/layout/_forms.scss b/webapp/sass/layout/_forms.scss index 259beeb57..1dd2bb827 100644 --- a/webapp/sass/layout/_forms.scss +++ b/webapp/sass/layout/_forms.scss @@ -12,7 +12,7 @@ text-align: left; &.light { - color: $dark-gray; + @include opacity(.6); font-size: 1.05em; font-style: italic; font-weight: normal; diff --git a/webapp/sass/layout/_post.scss b/webapp/sass/layout/_post.scss index 4170483db..947a81318 100644 --- a/webapp/sass/layout/_post.scss +++ b/webapp/sass/layout/_post.scss @@ -66,6 +66,7 @@ body.ios { font-size: 13px; position: absolute; right: 0; + text-align: right; z-index: 3; } @@ -74,16 +75,14 @@ body.ios { } } -.help_format_text { +.help__format-text { @include opacity(0); - @include single-transition(all .2s ease); - bottom: -23px; - display: none !important; + @include single-transition(all, .5s, ease, .5s); + display: inline-block; font-size: .85em; - left: 0; - overflow: hidden; - position: absolute; - text-overflow: ellipsis; + margin-right: 10px; + vertical-align: bottom; + white-space: nowrap; b, i, @@ -275,39 +274,36 @@ body.ios { outline: none; text-align: center; } - - .beginning-messages-text { - color: grey; - display: block; - margin-bottom: 5px; - margin-top: 2px; - text-align: center; - } } .post-list__timestamp { - @include border-radius(3px); @include opacity(0); @include single-transition(all, .6s, ease); @include translateY(-45px); - @include font-smoothing(initial); - background: $primary-color; - color: $white; display: none; - font-size: 12px; - left: 50%; - line-height: 25px; - margin-left: -60px; + left: 0; position: absolute; text-align: center; top: 8px; - width: 120px; + width: 100%; z-index: 50; &.scrolling { @include translateY(0); @include opacity(.8); } + + > div { + @include border-radius(3px); + @include font-smoothing(initial); + background: $primary-color; + color: $white; + display: inline-block; + font-size: 12px; + line-height: 25px; + padding: 0 8px; + text-align: center; + } } .post-list__arrows { @@ -381,6 +377,7 @@ body.ios { } .custom-textarea { + bottom: 0; line-height: 1.5; max-height: 162px !important; padding-right: 28px; @@ -647,6 +644,15 @@ body.ios { .post__img { width: 46px; + svg { + height: 36px; + width: 36px; + } + + path { + fill: inherit; + } + img { @include border-radius(50px); height: 36px; @@ -752,7 +758,7 @@ body.ios { li ul, li ol { - padding: 0 0 0 20px; + padding: 10px 0 0 20px; } li.list-item--task-list ul, diff --git a/webapp/sass/layout/_sidebar-right.scss b/webapp/sass/layout/_sidebar-right.scss index a7b631047..062c3bde1 100644 --- a/webapp/sass/layout/_sidebar-right.scss +++ b/webapp/sass/layout/_sidebar-right.scss @@ -25,6 +25,10 @@ } } + .help__format-text { + display: none; + } + .sidebar--right__content { @include display-flex; @include flex-direction(column); diff --git a/webapp/sass/responsive/_desktop.scss b/webapp/sass/responsive/_desktop.scss index ccd6f0226..3b36fb75f 100644 --- a/webapp/sass/responsive/_desktop.scss +++ b/webapp/sass/responsive/_desktop.scss @@ -17,6 +17,14 @@ } @media screen and (max-width: 1440px) { + .inner-wrap { + &.move--left { + .help__format-text { + display: none; + } + } + } + .date-separator, .new-separator { &.hovered--comment { @@ -40,6 +48,11 @@ } } + .backstage-content { + margin: 46px 46px 46px 150px; + } + + .inner-wrap { &.move--left { .file-overlay { diff --git a/webapp/sass/responsive/_mobile.scss b/webapp/sass/responsive/_mobile.scss index 0e1a471cf..38476485d 100644 --- a/webapp/sass/responsive/_mobile.scss +++ b/webapp/sass/responsive/_mobile.scss @@ -1,6 +1,16 @@ @charset 'UTF-8'; @media screen and (max-width: 768px) { + .backstage-filters { + display: block; + + .backstage-filter__search { + border-bottom: 1px solid $light-gray; + margin: 10px 0; + width: 100%; + } + } + .signup-team__container { font-size: 1em; } @@ -675,9 +685,9 @@ } .sidebar--right { - width: 100%; - right: 0; @include translate3d(100%, 0, 0); + right: 0; + width: 100%; z-index: 5; &.move--left { @@ -786,6 +796,40 @@ } @media screen and (max-width: 640px) { + .modal { + .about-modal { + .about-modal__content { + display: block; + } + + .about-modal__hash { + p { + word-break: break-all; + + &:first-child { + float: none; + } + } + } + + .about-modal__logo { + float: none; + padding: 0; + text-align: center; + width: 100%; + + svg { + height: 100px; + width: 100px; + } + } + + .about-modal__logo + div { + padding: 2em 0 0; + } + } + } + .access-history__table { > div { display: block; @@ -819,6 +863,30 @@ } @media screen and (max-width: 480px) { + .backstage-header { + h1 { + float: none; + margin-bottom: 15px; + } + + .add-integrations-link { + float: none; + } + } + + .add-integration { + width: 100%; + } + + .backstage-list__item { + display: block; + + .actions { + margin-top: 10px; + padding: 0; + } + } + .modal { .settings-modal { .settings-table { diff --git a/webapp/sass/responsive/_tablet.scss b/webapp/sass/responsive/_tablet.scss index 0a725a558..db2a8d7b9 100644 --- a/webapp/sass/responsive/_tablet.scss +++ b/webapp/sass/responsive/_tablet.scss @@ -15,6 +15,23 @@ } } + .backstage-content { + margin: 30px; + max-width: 100%; + padding: 0; + } + + .backstage-sidebar { + height: auto; + padding: 30px 15px 0; + position: relative; + width: 100%; + } + + .help__format-text { + display: none; + } + .inner-wrap { &.move--left { margin-right: 0; diff --git a/webapp/sass/routes/_about-modal.scss b/webapp/sass/routes/_about-modal.scss new file mode 100644 index 000000000..98119c8aa --- /dev/null +++ b/webapp/sass/routes/_about-modal.scss @@ -0,0 +1,78 @@ +@charset 'UTF-8'; + +.modal { + .about-modal { + .modal-header { + background: transparent; + border: none; + color: inherit; + padding: 20px 25px 0; + + .close { + color: inherit; + font-weight: normal; + right: 15px; + } + + .modal-title { + color: inherit; + font-size: 16px; + } + } + + .modal-body { + padding: 20px 25px 5px; + } + + .about-modal__content { + @include clearfix; + @include display-flex; + @include flex-direction(row); + padding: 1em 0 3em; + } + + .about-modal__copyright { + @include opacity(.6); + margin-top: .5em; + } + + .about-modal__footer { + font-size: 13.5px; + } + + .about-modal__title { + line-height: 1.5; + margin: 0 0 10px; + } + + .about-modal__subtitle { + @include opacity(.6); + } + + .about-modal__hash { + @include opacity(.4); + font-size: .75em; + text-align: right; + + p { + &:first-child { + float: left; + } + } + } + + .about-modal__logo { + @include opacity(.9); + padding: 0 40px 0 20px; + + svg { + height: 125px; + width: 125px; + } + + path { + fill: inherit; + } + } + } +} diff --git a/webapp/sass/routes/_backstage.scss b/webapp/sass/routes/_backstage.scss new file mode 100644 index 000000000..729c8c912 --- /dev/null +++ b/webapp/sass/routes/_backstage.scss @@ -0,0 +1,267 @@ +.backstage-content { + background-color: $bg--gray; + height: 100%; + margin: 46px auto; + max-width: 960px; + padding-left: 135px; +} + +.backstage-navbar { + background: $white; + border-bottom: 1px solid $light-gray; + padding: 10px 20px; + z-index: 10; +} + +.backstage-navbar__back { + color: inherit; + text-decoration: none; + + .fa { + font-size: 1.1em; + font-weight: bold; + margin-right: 7px; + } + + &:hover, + &:active { + color: inherit; + } +} + +.backstage-sidebar { + height: 100%; + left: 0; + padding: 50px 20px; + position: absolute; + width: 260px; + z-index: 5; + + ul { + list-style: none; + padding: 0; + } +} + +.backstage-sidebar__category { + border: 1px solid $light-gray; + + .category-title { + display: block; + line-height: 36px; + padding: 0 10px; + position: relative; + } + + .category-title--active { + color: $black; + } + + .category-title__text { + left: 2em; + position: absolute; + } + + .sections { + background: $white; + border-top: 1px solid $light-gray; + } + + .section-title, + .subsection-title { + display: block; + font-size: .95em; + line-height: 29px; + padding-left: 2em; + text-decoration: none; + } + + .subsection-title { + padding-left: 3em; + } + + .section-title--active, + .subsection-title--active { + background-color: $primary-color; + color: $white; + font-weight: 600; + } +} + +.backstage__sidebar__category + .backstage__sidebar__category { + border-top-width: 0; +} + +.backstage-header { + @include clearfix; + margin-bottom: 20px; + width: 100%; + + h1 { + float: left; + font-size: 20px; + margin: 5px 0; + } + + .add-integrations-link { + float: right; + } +} + +.backstage-filters { + display: flex; + flex-direction: row; + width: 100%; + + .backstage-filters__sort { + flex-grow: 1; + flex-shrink: 0; + line-height: 30px; + + .filter-sort { + text-decoration: none; + + &.filter-sort--active { + color: inherit; + cursor: default; + } + } + + .divider { + margin-left: 8px; + margin-right: 8px; + } + } + + .backstage-filter__search { + flex-grow: 0; + flex-shrink: 0; + position: relative; + width: 270px; + + .fa { + @include opacity(.4); + left: 11px; + position: absolute; + top: 11px; + } + + input { + background: $white; + border-bottom: none; + padding-left: 30px; + } + } +} + +.backstage-list { + background-color: $white; + border: 1px solid $light-gray; + padding: 5px 15px; +} + +.backstage-list__item { + border-bottom: 1px solid $light-gray; + display: flex; + padding: 20px 15px; + + &:last-child { + border: none; + } + + .item-details { + flex-grow: 1; + flex-shrink: 1; + overflow: hidden; + text-overflow: ellipsis; + } + + .item-details__row + .item-details__row { + @include clearfix; + margin-top: 10px; + text-overflow: ellipsis; + } + + .item-details__name { + font-weight: 600; + margin-bottom: 1em; + } + + .item-details__type { + margin-left: 6px; + } + + .item-details__description { + color: $dark-gray; + margin-bottom: 1em; + } + + .list-item__actions { + flex-grow: 0; + flex-shrink: 0; + padding-left: 20px; + } +} + +// Backstage Form + +.backstage-form { + background-color: $white; + border: 1px solid $light-gray; + padding: 40px 30px 30px; + + label { + font-weight: normal; + } + + .form-control { + background: $white; + + &:focus { + border-color: $primary-color; + } + } +} + +.backstage-form__footer { + border-top: 1px solid $light-gray; + margin-top: 2.5em; + padding-top: 1.8em; + text-align: right; + + .has-error { + float: left; + margin: 0; + } +} + +.add-integration { + background-color: $white; + border: 1px solid $light-gray; + display: inline-block; + height: 210px; + margin: 0 30px 20px 0; + padding: 20px; + text-align: center; + vertical-align: top; + width: 250px; + + &:hover { + color: default; + text-decoration: none; + } +} + +.add-integration__image { + height: 80px; + width: 80px; +} + +.add-integration__title { + color: $black; + margin-bottom: 10px; +} + +.add-integration__description { + color: $dark-gray; +} diff --git a/webapp/sass/routes/_module.scss b/webapp/sass/routes/_module.scss index 48c1af1d9..4f3f6f9cd 100644 --- a/webapp/sass/routes/_module.scss +++ b/webapp/sass/routes/_module.scss @@ -1,7 +1,9 @@ // Only for combining all the files in this folder +@import 'about-modal'; @import 'access-history'; @import 'activity-log'; @import 'admin-console'; +@import 'backstage'; @import 'docs'; @import 'error-page'; @import 'loading'; diff --git a/webapp/sass/routes/_settings.scss b/webapp/sass/routes/_settings.scss index 1c3f2e308..1551e5f4d 100644 --- a/webapp/sass/routes/_settings.scss +++ b/webapp/sass/routes/_settings.scss @@ -51,38 +51,6 @@ padding-left: 0; } - .padding-top { - padding-top: 7px; - - &.x2 { - padding-top: 14px; - } - - &.x3 { - padding-top: 21px; - } - } - - .padding-bottom { - padding-bottom: 7px; - - &.x2 { - padding-bottom: 14px; - } - - &.x3 { - padding-bottom: 21px; - } - - .control-label { - font-weight: 600; - - &.text-left { - text-align: left; - } - } - } - .profile-img { height: 128px; width: 128px; diff --git a/webapp/sass/routes/_signup.scss b/webapp/sass/routes/_signup.scss index 09f8e4185..6d6092170 100644 --- a/webapp/sass/routes/_signup.scss +++ b/webapp/sass/routes/_signup.scss @@ -98,7 +98,6 @@ .inner__content { margin: 30px 0 20px; - padding: 0 1rem; } .block--gray { @@ -132,14 +131,12 @@ font-size: 2.2em; font-weight: 600; margin: .5em 0 0; - padding-left: 1rem; } .signup-team__subdomain { font-size: 1.5em; font-weight: 300; margin: .2em 0 1.2em; - padding-left: 1rem; text-transform: uppercase; } @@ -151,6 +148,7 @@ background: #dddddd; height: 1px; margin: 2em 0; + margin: 2.5em 0 2.5em -1rem; text-align: left; span { @@ -171,10 +169,6 @@ padding-left: 18px; } - .signup__email-container { - margin-left: 1rem; - } - .btn { font-size: 1em; font-weight: 600; @@ -200,7 +194,7 @@ display: block; height: 40px; line-height: 34px; - margin: 1em 1rem; + margin: 1em 0; min-width: 200px; padding: 0 1em; width: 200px; @@ -262,9 +256,9 @@ } &.btn-full { - width: 100%; - text-align: left; padding-left: 35px; + text-align: left; + width: 100%; } } @@ -373,11 +367,11 @@ } .margin--extra { - margin-top: 3em; + margin-top: 2.5em; } .margin--extra-2x { - margin-top: 6em; + margin-top: 5em; } } diff --git a/webapp/sass/styles.scss b/webapp/sass/styles.scss index 88c098f18..67e62d023 100644 --- a/webapp/sass/styles.scss +++ b/webapp/sass/styles.scss @@ -1,18 +1,19 @@ -@charset "UTF-8"; +@charset 'UTF-8'; -/* Welcome to Compass. - * In this file you should write your main styles. (or centralize your imports) - * Import this file using the following HTML or equivalent: - * <link href="/stylesheets/screen.css" media="screen, projection" rel="stylesheet" type="text/css" /> */ +@import 'compass/utilities'; +@import 'compass/css3'; -@import "compass/utilities"; -@import "compass/css3"; +// Dependancies +@import '~bootstrap/dist/css/bootstrap.css'; +@import '~jasny-bootstrap/dist/css/jasny-bootstrap.css'; +@import '~perfect-scrollbar/dist/css/perfect-scrollbar.css'; +@import '~font-awesome/css/font-awesome.css'; +@import '~bootstrap-colorpicker/dist/css/bootstrap-colorpicker.css'; // styles.scss -@import 'vendors/module'; @import 'utils/module'; @import 'base/module'; @import 'routes/module'; @import 'components/module'; @import 'layout/module'; -@import 'responsive/module';
\ No newline at end of file +@import 'responsive/module'; diff --git a/webapp/sass/utils/_variables.scss b/webapp/sass/utils/_variables.scss index 345ab11e8..53004520e 100644 --- a/webapp/sass/utils/_variables.scss +++ b/webapp/sass/utils/_variables.scss @@ -8,7 +8,7 @@ $white: rgb(255, 255, 255); $black: rgb(0, 0, 0); $red: rgb(229, 101, 101); $yellow: rgb(255, 255, 0); -$light-gray: rgba(0, 0, 0, .06); +$light-gray: rgba(0, 0, 0, .15); $gray: rgba(0, 0, 0, .3); $dark-gray: rgba(0, 0, 0, .5); diff --git a/webapp/sass/vendors/_colorpicker.scss b/webapp/sass/vendors/_colorpicker.scss deleted file mode 100644 index 291145e80..000000000 --- a/webapp/sass/vendors/_colorpicker.scss +++ /dev/null @@ -1,253 +0,0 @@ -@charset 'UTF-8'; - -/*! - * Bootstrap Colorpicker - * http://mjolnic.github.io/bootstrap-colorpicker/ - * - * Originally written by (c) 2012 Stefan Petre - * Licensed under the Apache License v2.0 - * http://www.apache.org/licenses/LICENSE-2.0.txt - * - */ - -.colorpicker-saturation { - float: left; - width: 100px; - height: 100px; - cursor: crosshair; - background-image: url('../images/bootstrap-colorpicker/saturation.png'); -} - -.colorpicker-saturation i { - position: absolute; - top: 0; - left: 0; - display: block; - width: 5px; - height: 5px; - margin: -4px 0 0 -4px; - border: 1px solid #000; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - border-radius: 5px; -} - -.colorpicker-saturation i b { - display: block; - width: 5px; - height: 5px; - border: 1px solid #fff; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - border-radius: 5px; -} - -.colorpicker-hue, -.colorpicker-alpha { - float: left; - width: 15px; - height: 100px; - margin-bottom: 4px; - margin-left: 4px; - cursor: row-resize; -} - -.colorpicker-hue i, -.colorpicker-alpha i { - position: absolute; - top: 0; - left: 0; - display: block; - width: 100%; - height: 1px; - margin-top: -1px; - background: #000; - border-top: 1px solid #fff; -} - -.colorpicker-hue { - background-image: url('../images/bootstrap-colorpicker/hue.png'); -} - -.colorpicker-alpha { - display: none; - background-image: url('../images/bootstrap-colorpicker/alpha.png'); -} - -.colorpicker-saturation, -.colorpicker-hue, -.colorpicker-alpha { - background-size: contain; -} - -.colorpicker { - top: 0; - left: 0; - z-index: 2500; - min-width: 130px; - padding: 4px; - margin-top: 1px; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - *zoom: 1; -} - -.colorpicker:before, -.colorpicker:after { - display: table; - line-height: 0; - content: ''; -} - -.colorpicker:after { - clear: both; -} - -.colorpicker:before { - position: absolute; - top: -7px; - left: 6px; - display: inline-block; - border-right: 7px solid transparent; - border-bottom: 7px solid #ccc; - border-left: 7px solid transparent; - border-bottom-color: rgba(0, 0, 0, .2); - content: ''; -} - -.colorpicker:after { - position: absolute; - top: -6px; - left: 7px; - display: inline-block; - border-right: 6px solid transparent; - border-bottom: 6px solid #fff; - border-left: 6px solid transparent; - content: ''; -} - -.colorpicker div { - position: relative; -} - -.colorpicker.colorpicker-with-alpha { - min-width: 140px; -} - -.colorpicker.colorpicker-with-alpha .colorpicker-alpha { - display: block; -} - -.colorpicker-color { - height: 10px; - margin-top: 5px; - clear: both; - background-image: url('../images/bootstrap-colorpicker/alpha.png'); - background-position: 0 100%; -} - -.colorpicker-color div { - height: 10px; -} - -.colorpicker-selectors { - display: none; - height: 10px; - margin-top: 5px; - clear: both; -} - -.colorpicker-selectors i { - float: left; - width: 10px; - height: 10px; - cursor: pointer; -} - -.colorpicker-selectors i + i { - margin-left: 3px; -} - -.colorpicker-element .input-group-addon i, -.colorpicker-element .add-on i { - display: inline-block; - width: 16px; - height: 16px; - vertical-align: text-top; - cursor: pointer; -} - -.colorpicker.colorpicker-inline { - position: relative; - z-index: auto; - display: inline-block; - float: none; -} - -.colorpicker.colorpicker-horizontal { - width: 110px; - height: auto; - min-width: 110px; -} - -.colorpicker.colorpicker-horizontal .colorpicker-saturation { - margin-bottom: 4px; -} - -.colorpicker.colorpicker-horizontal .colorpicker-color { - width: 100px; -} - -.colorpicker.colorpicker-horizontal .colorpicker-hue, -.colorpicker.colorpicker-horizontal .colorpicker-alpha { - float: left; - width: 100px; - height: 15px; - margin-bottom: 4px; - margin-left: 0; - cursor: col-resize; -} - -.colorpicker.colorpicker-horizontal .colorpicker-hue i, -.colorpicker.colorpicker-horizontal .colorpicker-alpha i { - position: absolute; - top: 0; - left: 0; - display: block; - width: 1px; - height: 15px; - margin-top: 0; - background: #fff; - border: none; -} - -.colorpicker.colorpicker-horizontal .colorpicker-hue { - background-image: url('../images/bootstrap-colorpicker/hue-horizontal.png'); -} - -.colorpicker.colorpicker-horizontal .colorpicker-alpha { - background-image: url('../images/bootstrap-colorpicker/alpha-horizontal.png'); -} - -.colorpicker.colorpicker-hidden { - display: none; -} - -.colorpicker.colorpicker-visible { - display: block; -} - -.colorpicker-inline.colorpicker-visible { - display: inline-block; -} - -.colorpicker-right:before { - right: 6px; - left: auto; -} - -.colorpicker-right:after { - right: 7px; - left: auto; -} diff --git a/webapp/sass/vendors/_font-awesome.scss b/webapp/sass/vendors/_font-awesome.scss deleted file mode 100644 index 49ab318cd..000000000 --- a/webapp/sass/vendors/_font-awesome.scss +++ /dev/null @@ -1,1803 +0,0 @@ -@charset 'UTF-8'; - -/*! - * Font Awesome 4.3.0 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */ -/* FONT PATH - * -------------------------- */ -@font-face { - font-family: 'FontAwesome'; - src: url('../fonts/fontawesome-webfont.eot?v=4.3.0'); - src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.3.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.3.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.3.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.3.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.3.0#fontawesomeregular') format('svg'); - font-weight: normal; - font-style: normal; -} -.fa { - display: inline-block; - font: normal normal normal 14px/1 FontAwesome; - font-size: inherit; - text-rendering: auto; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - transform: translate(0, 0); -} -/* makes the font 33% larger relative to the icon container */ -.fa-lg { - font-size: 1.33333333em; - line-height: .75em; - vertical-align: -15%; -} -.fa-2x { - font-size: 2em; -} -.fa-3x { - font-size: 3em; -} -.fa-4x { - font-size: 4em; -} -.fa-5x { - font-size: 5em; -} -.fa-fw { - width: 1.28571429em; - text-align: center; -} -.fa-ul { - padding-left: 0; - margin-left: 2.14285714em; - list-style-type: none; -} -.fa-ul > li { - position: relative; -} -.fa-li { - position: absolute; - left: -2.14285714em; - width: 2.14285714em; - top: .14285714em; - text-align: center; -} -.fa-li.fa-lg { - left: -1.85714286em; -} -.fa-border { - padding: .2em .25em .15em; - border: solid .08em #eee; - border-radius: .1em; -} -.pull-right { - float: right; -} -.pull-left { - float: left; -} -.fa.pull-left { - margin-right: .3em; -} -.fa.pull-right { - margin-left: .3em; -} -.fa-spin { - -webkit-animation: fa-spin 2s infinite linear; - animation: fa-spin 2s infinite linear; -} -.fa-pulse { - -webkit-animation: fa-spin 1s infinite steps(8); - animation: fa-spin 1s infinite steps(8); -} -@-webkit-keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} -@keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} -.fa-rotate-90 { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); - -webkit-transform: rotate(90deg); - -ms-transform: rotate(90deg); - transform: rotate(90deg); -} -.fa-rotate-180 { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); - -webkit-transform: rotate(180deg); - -ms-transform: rotate(180deg); - transform: rotate(180deg); -} -.fa-rotate-270 { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); - -webkit-transform: rotate(270deg); - -ms-transform: rotate(270deg); - transform: rotate(270deg); -} -.fa-flip-horizontal { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1); - -webkit-transform: scale(-1, 1); - -ms-transform: scale(-1, 1); - transform: scale(-1, 1); -} -.fa-flip-vertical { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1); - -webkit-transform: scale(1, -1); - -ms-transform: scale(1, -1); - transform: scale(1, -1); -} -:root .fa-rotate-90, -:root .fa-rotate-180, -:root .fa-rotate-270, -:root .fa-flip-horizontal, -:root .fa-flip-vertical { - filter: none; -} -.fa-stack { - position: relative; - display: inline-block; - width: 2em; - height: 2em; - line-height: 2em; - vertical-align: middle; -} -.fa-stack-1x, -.fa-stack-2x { - position: absolute; - left: 0; - width: 100%; - text-align: center; -} -.fa-stack-1x { - line-height: inherit; -} -.fa-stack-2x { - font-size: 2em; -} -.fa-inverse { - color: #fff; -} -/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen - readers do not read off random characters that represent icons */ -.fa-glass:before { - content: '\f000'; -} -.fa-music:before { - content: '\f001'; -} -.fa-search:before { - content: '\f002'; -} -.fa-envelope-o:before { - content: '\f003'; -} -.fa-heart:before { - content: '\f004'; -} -.fa-star:before { - content: '\f005'; -} -.fa-star-o:before { - content: '\f006'; -} -.fa-user:before { - content: '\f007'; -} -.fa-film:before { - content: '\f008'; -} -.fa-th-large:before { - content: '\f009'; -} -.fa-th:before { - content: '\f00a'; -} -.fa-th-list:before { - content: '\f00b'; -} -.fa-check:before { - content: '\f00c'; -} -.fa-remove:before, -.fa-close:before, -.fa-times:before { - content: '\f00d'; -} -.fa-search-plus:before { - content: '\f00e'; -} -.fa-search-minus:before { - content: '\f010'; -} -.fa-power-off:before { - content: '\f011'; -} -.fa-signal:before { - content: '\f012'; -} -.fa-gear:before, -.fa-cog:before { - content: '\f013'; -} -.fa-trash-o:before { - content: '\f014'; -} -.fa-home:before { - content: '\f015'; -} -.fa-file-o:before { - content: '\f016'; -} -.fa-clock-o:before { - content: '\f017'; -} -.fa-road:before { - content: '\f018'; -} -.fa-download:before { - content: '\f019'; -} -.fa-arrow-circle-o-down:before { - content: '\f01a'; -} -.fa-arrow-circle-o-up:before { - content: '\f01b'; -} -.fa-inbox:before { - content: '\f01c'; -} -.fa-play-circle-o:before { - content: '\f01d'; -} -.fa-rotate-right:before, -.fa-repeat:before { - content: '\f01e'; -} -.fa-refresh:before { - content: '\f021'; -} -.fa-list-alt:before { - content: '\f022'; -} -.fa-lock:before { - content: '\f023'; -} -.fa-flag:before { - content: '\f024'; -} -.fa-headphones:before { - content: '\f025'; -} -.fa-volume-off:before { - content: '\f026'; -} -.fa-volume-down:before { - content: '\f027'; -} -.fa-volume-up:before { - content: '\f028'; -} -.fa-qrcode:before { - content: '\f029'; -} -.fa-barcode:before { - content: '\f02a'; -} -.fa-tag:before { - content: '\f02b'; -} -.fa-tags:before { - content: '\f02c'; -} -.fa-book:before { - content: '\f02d'; -} -.fa-bookmark:before { - content: '\f02e'; -} -.fa-print:before { - content: '\f02f'; -} -.fa-camera:before { - content: '\f030'; -} -.fa-font:before { - content: '\f031'; -} -.fa-bold:before { - content: '\f032'; -} -.fa-italic:before { - content: '\f033'; -} -.fa-text-height:before { - content: '\f034'; -} -.fa-text-width:before { - content: '\f035'; -} -.fa-align-left:before { - content: '\f036'; -} -.fa-align-center:before { - content: '\f037'; -} -.fa-align-right:before { - content: '\f038'; -} -.fa-align-justify:before { - content: '\f039'; -} -.fa-list:before { - content: '\f03a'; -} -.fa-dedent:before, -.fa-outdent:before { - content: '\f03b'; -} -.fa-indent:before { - content: '\f03c'; -} -.fa-video-camera:before { - content: '\f03d'; -} -.fa-photo:before, -.fa-image:before, -.fa-picture-o:before { - content: '\f03e'; -} -.fa-pencil:before { - content: '\f040'; -} -.fa-map-marker:before { - content: '\f041'; -} -.fa-adjust:before { - content: '\f042'; -} -.fa-tint:before { - content: '\f043'; -} -.fa-edit:before, -.fa-pencil-square-o:before { - content: '\f044'; -} -.fa-share-square-o:before { - content: '\f045'; -} -.fa-check-square-o:before { - content: '\f046'; -} -.fa-arrows:before { - content: '\f047'; -} -.fa-step-backward:before { - content: '\f048'; -} -.fa-fast-backward:before { - content: '\f049'; -} -.fa-backward:before { - content: '\f04a'; -} -.fa-play:before { - content: '\f04b'; -} -.fa-pause:before { - content: '\f04c'; -} -.fa-stop:before { - content: '\f04d'; -} -.fa-forward:before { - content: '\f04e'; -} -.fa-fast-forward:before { - content: '\f050'; -} -.fa-step-forward:before { - content: '\f051'; -} -.fa-eject:before { - content: '\f052'; -} -.fa-chevron-left:before { - content: '\f053'; -} -.fa-chevron-right:before { - content: '\f054'; -} -.fa-plus-circle:before { - content: '\f055'; -} -.fa-minus-circle:before { - content: '\f056'; -} -.fa-times-circle:before { - content: '\f057'; -} -.fa-check-circle:before { - content: '\f058'; -} -.fa-question-circle:before { - content: '\f059'; -} -.fa-info-circle:before { - content: '\f05a'; -} -.fa-crosshairs:before { - content: '\f05b'; -} -.fa-times-circle-o:before { - content: '\f05c'; -} -.fa-check-circle-o:before { - content: '\f05d'; -} -.fa-ban:before { - content: '\f05e'; -} -.fa-arrow-left:before { - content: '\f060'; -} -.fa-arrow-right:before { - content: '\f061'; -} -.fa-arrow-up:before { - content: '\f062'; -} -.fa-arrow-down:before { - content: '\f063'; -} -.fa-mail-forward:before, -.fa-share:before { - content: '\f064'; -} -.fa-expand:before { - content: '\f065'; -} -.fa-compress:before { - content: '\f066'; -} -.fa-plus:before { - content: '\f067'; -} -.fa-minus:before { - content: '\f068'; -} -.fa-asterisk:before { - content: '\f069'; -} -.fa-exclamation-circle:before { - content: '\f06a'; -} -.fa-gift:before { - content: '\f06b'; -} -.fa-leaf:before { - content: '\f06c'; -} -.fa-fire:before { - content: '\f06d'; -} -.fa-eye:before { - content: '\f06e'; -} -.fa-eye-slash:before { - content: '\f070'; -} -.fa-warning:before, -.fa-exclamation-triangle:before { - content: '\f071'; -} -.fa-plane:before { - content: '\f072'; -} -.fa-calendar:before { - content: '\f073'; -} -.fa-random:before { - content: '\f074'; -} -.fa-comment:before { - content: '\f075'; -} -.fa-magnet:before { - content: '\f076'; -} -.fa-chevron-up:before { - content: '\f077'; -} -.fa-chevron-down:before { - content: '\f078'; -} -.fa-retweet:before { - content: '\f079'; -} -.fa-shopping-cart:before { - content: '\f07a'; -} -.fa-folder:before { - content: '\f07b'; -} -.fa-folder-open:before { - content: '\f07c'; -} -.fa-arrows-v:before { - content: '\f07d'; -} -.fa-arrows-h:before { - content: '\f07e'; -} -.fa-bar-chart-o:before, -.fa-bar-chart:before { - content: '\f080'; -} -.fa-twitter-square:before { - content: '\f081'; -} -.fa-facebook-square:before { - content: '\f082'; -} -.fa-camera-retro:before { - content: '\f083'; -} -.fa-key:before { - content: '\f084'; -} -.fa-gears:before, -.fa-cogs:before { - content: '\f085'; -} -.fa-comments:before { - content: '\f086'; -} -.fa-thumbs-o-up:before { - content: '\f087'; -} -.fa-thumbs-o-down:before { - content: '\f088'; -} -.fa-star-half:before { - content: '\f089'; -} -.fa-heart-o:before { - content: '\f08a'; -} -.fa-sign-out:before { - content: '\f08b'; -} -.fa-linkedin-square:before { - content: '\f08c'; -} -.fa-thumb-tack:before { - content: '\f08d'; -} -.fa-external-link:before { - content: '\f08e'; -} -.fa-sign-in:before { - content: '\f090'; -} -.fa-trophy:before { - content: '\f091'; -} -.fa-github-square:before { - content: '\f092'; -} -.fa-upload:before { - content: '\f093'; -} -.fa-lemon-o:before { - content: '\f094'; -} -.fa-phone:before { - content: '\f095'; -} -.fa-square-o:before { - content: '\f096'; -} -.fa-bookmark-o:before { - content: '\f097'; -} -.fa-phone-square:before { - content: '\f098'; -} -.fa-twitter:before { - content: '\f099'; -} -.fa-facebook-f:before, -.fa-facebook:before { - content: '\f09a'; -} -.fa-github:before { - content: '\f09b'; -} -.fa-unlock:before { - content: '\f09c'; -} -.fa-credit-card:before { - content: '\f09d'; -} -.fa-rss:before { - content: '\f09e'; -} -.fa-hdd-o:before { - content: '\f0a0'; -} -.fa-bullhorn:before { - content: '\f0a1'; -} -.fa-bell:before { - content: '\f0f3'; -} -.fa-certificate:before { - content: '\f0a3'; -} -.fa-hand-o-right:before { - content: '\f0a4'; -} -.fa-hand-o-left:before { - content: '\f0a5'; -} -.fa-hand-o-up:before { - content: '\f0a6'; -} -.fa-hand-o-down:before { - content: '\f0a7'; -} -.fa-arrow-circle-left:before { - content: '\f0a8'; -} -.fa-arrow-circle-right:before { - content: '\f0a9'; -} -.fa-arrow-circle-up:before { - content: '\f0aa'; -} -.fa-arrow-circle-down:before { - content: '\f0ab'; -} -.fa-globe:before { - content: '\f0ac'; -} -.fa-wrench:before { - content: '\f0ad'; -} -.fa-tasks:before { - content: '\f0ae'; -} -.fa-filter:before { - content: '\f0b0'; -} -.fa-briefcase:before { - content: '\f0b1'; -} -.fa-arrows-alt:before { - content: '\f0b2'; -} -.fa-group:before, -.fa-users:before { - content: '\f0c0'; -} -.fa-chain:before, -.fa-link:before { - content: '\f0c1'; -} -.fa-cloud:before { - content: '\f0c2'; -} -.fa-flask:before { - content: '\f0c3'; -} -.fa-cut:before, -.fa-scissors:before { - content: '\f0c4'; -} -.fa-copy:before, -.fa-files-o:before { - content: '\f0c5'; -} -.fa-paperclip:before { - content: '\f0c6'; -} -.fa-save:before, -.fa-floppy-o:before { - content: '\f0c7'; -} -.fa-square:before { - content: '\f0c8'; -} -.fa-navicon:before, -.fa-reorder:before, -.fa-bars:before { - content: '\f0c9'; -} -.fa-list-ul:before { - content: '\f0ca'; -} -.fa-list-ol:before { - content: '\f0cb'; -} -.fa-strikethrough:before { - content: '\f0cc'; -} -.fa-underline:before { - content: '\f0cd'; -} -.fa-table:before { - content: '\f0ce'; -} -.fa-magic:before { - content: '\f0d0'; -} -.fa-truck:before { - content: '\f0d1'; -} -.fa-pinterest:before { - content: '\f0d2'; -} -.fa-pinterest-square:before { - content: '\f0d3'; -} -.fa-google-plus-square:before { - content: '\f0d4'; -} -.fa-google-plus:before { - content: '\f0d5'; -} -.fa-money:before { - content: '\f0d6'; -} -.fa-caret-down:before { - content: '\f0d7'; -} -.fa-caret-up:before { - content: '\f0d8'; -} -.fa-caret-left:before { - content: '\f0d9'; -} -.fa-caret-right:before { - content: '\f0da'; -} -.fa-columns:before { - content: '\f0db'; -} -.fa-unsorted:before, -.fa-sort:before { - content: '\f0dc'; -} -.fa-sort-down:before, -.fa-sort-desc:before { - content: '\f0dd'; -} -.fa-sort-up:before, -.fa-sort-asc:before { - content: '\f0de'; -} -.fa-envelope:before { - content: '\f0e0'; -} -.fa-linkedin:before { - content: '\f0e1'; -} -.fa-rotate-left:before, -.fa-undo:before { - content: '\f0e2'; -} -.fa-legal:before, -.fa-gavel:before { - content: '\f0e3'; -} -.fa-dashboard:before, -.fa-tachometer:before { - content: '\f0e4'; -} -.fa-comment-o:before { - content: '\f0e5'; -} -.fa-comments-o:before { - content: '\f0e6'; -} -.fa-flash:before, -.fa-bolt:before { - content: '\f0e7'; -} -.fa-sitemap:before { - content: '\f0e8'; -} -.fa-umbrella:before { - content: '\f0e9'; -} -.fa-paste:before, -.fa-clipboard:before { - content: '\f0ea'; -} -.fa-lightbulb-o:before { - content: '\f0eb'; -} -.fa-exchange:before { - content: '\f0ec'; -} -.fa-cloud-download:before { - content: '\f0ed'; -} -.fa-cloud-upload:before { - content: '\f0ee'; -} -.fa-user-md:before { - content: '\f0f0'; -} -.fa-stethoscope:before { - content: '\f0f1'; -} -.fa-suitcase:before { - content: '\f0f2'; -} -.fa-bell-o:before { - content: '\f0a2'; -} -.fa-coffee:before { - content: '\f0f4'; -} -.fa-cutlery:before { - content: '\f0f5'; -} -.fa-file-text-o:before { - content: '\f0f6'; -} -.fa-building-o:before { - content: '\f0f7'; -} -.fa-hospital-o:before { - content: '\f0f8'; -} -.fa-ambulance:before { - content: '\f0f9'; -} -.fa-medkit:before { - content: '\f0fa'; -} -.fa-fighter-jet:before { - content: '\f0fb'; -} -.fa-beer:before { - content: '\f0fc'; -} -.fa-h-square:before { - content: '\f0fd'; -} -.fa-plus-square:before { - content: '\f0fe'; -} -.fa-angle-double-left:before { - content: '\f100'; -} -.fa-angle-double-right:before { - content: '\f101'; -} -.fa-angle-double-up:before { - content: '\f102'; -} -.fa-angle-double-down:before { - content: '\f103'; -} -.fa-angle-left:before { - content: '\f104'; -} -.fa-angle-right:before { - content: '\f105'; -} -.fa-angle-up:before { - content: '\f106'; -} -.fa-angle-down:before { - content: '\f107'; -} -.fa-desktop:before { - content: '\f108'; -} -.fa-laptop:before { - content: '\f109'; -} -.fa-tablet:before { - content: '\f10a'; -} -.fa-mobile-phone:before, -.fa-mobile:before { - content: '\f10b'; -} -.fa-circle-o:before { - content: '\f10c'; -} -.fa-quote-left:before { - content: '\f10d'; -} -.fa-quote-right:before { - content: '\f10e'; -} -.fa-spinner:before { - content: '\f110'; -} -.fa-circle:before { - content: '\f111'; -} -.fa-mail-reply:before, -.fa-reply:before { - content: '\f112'; -} -.fa-github-alt:before { - content: '\f113'; -} -.fa-folder-o:before { - content: '\f114'; -} -.fa-folder-open-o:before { - content: '\f115'; -} -.fa-smile-o:before { - content: '\f118'; -} -.fa-frown-o:before { - content: '\f119'; -} -.fa-meh-o:before { - content: '\f11a'; -} -.fa-gamepad:before { - content: '\f11b'; -} -.fa-keyboard-o:before { - content: '\f11c'; -} -.fa-flag-o:before { - content: '\f11d'; -} -.fa-flag-checkered:before { - content: '\f11e'; -} -.fa-terminal:before { - content: '\f120'; -} -.fa-code:before { - content: '\f121'; -} -.fa-mail-reply-all:before, -.fa-reply-all:before { - content: '\f122'; -} -.fa-star-half-empty:before, -.fa-star-half-full:before, -.fa-star-half-o:before { - content: '\f123'; -} -.fa-location-arrow:before { - content: '\f124'; -} -.fa-crop:before { - content: '\f125'; -} -.fa-code-fork:before { - content: '\f126'; -} -.fa-unlink:before, -.fa-chain-broken:before { - content: '\f127'; -} -.fa-question:before { - content: '\f128'; -} -.fa-info:before { - content: '\f129'; -} -.fa-exclamation:before { - content: '\f12a'; -} -.fa-superscript:before { - content: '\f12b'; -} -.fa-subscript:before { - content: '\f12c'; -} -.fa-eraser:before { - content: '\f12d'; -} -.fa-puzzle-piece:before { - content: '\f12e'; -} -.fa-microphone:before { - content: '\f130'; -} -.fa-microphone-slash:before { - content: '\f131'; -} -.fa-shield:before { - content: '\f132'; -} -.fa-calendar-o:before { - content: '\f133'; -} -.fa-fire-extinguisher:before { - content: '\f134'; -} -.fa-rocket:before { - content: '\f135'; -} -.fa-maxcdn:before { - content: '\f136'; -} -.fa-chevron-circle-left:before { - content: '\f137'; -} -.fa-chevron-circle-right:before { - content: '\f138'; -} -.fa-chevron-circle-up:before { - content: '\f139'; -} -.fa-chevron-circle-down:before { - content: '\f13a'; -} -.fa-html5:before { - content: '\f13b'; -} -.fa-css3:before { - content: '\f13c'; -} -.fa-anchor:before { - content: '\f13d'; -} -.fa-unlock-alt:before { - content: '\f13e'; -} -.fa-bullseye:before { - content: '\f140'; -} -.fa-ellipsis-h:before { - content: '\f141'; -} -.fa-ellipsis-v:before { - content: '\f142'; -} -.fa-rss-square:before { - content: '\f143'; -} -.fa-play-circle:before { - content: '\f144'; -} -.fa-ticket:before { - content: '\f145'; -} -.fa-minus-square:before { - content: '\f146'; -} -.fa-minus-square-o:before { - content: '\f147'; -} -.fa-level-up:before { - content: '\f148'; -} -.fa-level-down:before { - content: '\f149'; -} -.fa-check-square:before { - content: '\f14a'; -} -.fa-pencil-square:before { - content: '\f14b'; -} -.fa-external-link-square:before { - content: '\f14c'; -} -.fa-share-square:before { - content: '\f14d'; -} -.fa-compass:before { - content: '\f14e'; -} -.fa-toggle-down:before, -.fa-caret-square-o-down:before { - content: '\f150'; -} -.fa-toggle-up:before, -.fa-caret-square-o-up:before { - content: '\f151'; -} -.fa-toggle-right:before, -.fa-caret-square-o-right:before { - content: '\f152'; -} -.fa-euro:before, -.fa-eur:before { - content: '\f153'; -} -.fa-gbp:before { - content: '\f154'; -} -.fa-dollar:before, -.fa-usd:before { - content: '\f155'; -} -.fa-rupee:before, -.fa-inr:before { - content: '\f156'; -} -.fa-cny:before, -.fa-rmb:before, -.fa-yen:before, -.fa-jpy:before { - content: '\f157'; -} -.fa-ruble:before, -.fa-rouble:before, -.fa-rub:before { - content: '\f158'; -} -.fa-won:before, -.fa-krw:before { - content: '\f159'; -} -.fa-bitcoin:before, -.fa-btc:before { - content: '\f15a'; -} -.fa-file:before { - content: '\f15b'; -} -.fa-file-text:before { - content: '\f15c'; -} -.fa-sort-alpha-asc:before { - content: '\f15d'; -} -.fa-sort-alpha-desc:before { - content: '\f15e'; -} -.fa-sort-amount-asc:before { - content: '\f160'; -} -.fa-sort-amount-desc:before { - content: '\f161'; -} -.fa-sort-numeric-asc:before { - content: '\f162'; -} -.fa-sort-numeric-desc:before { - content: '\f163'; -} -.fa-thumbs-up:before { - content: '\f164'; -} -.fa-thumbs-down:before { - content: '\f165'; -} -.fa-youtube-square:before { - content: '\f166'; -} -.fa-youtube:before { - content: '\f167'; -} -.fa-xing:before { - content: '\f168'; -} -.fa-xing-square:before { - content: '\f169'; -} -.fa-youtube-play:before { - content: '\f16a'; -} -.fa-dropbox:before { - content: '\f16b'; -} -.fa-stack-overflow:before { - content: '\f16c'; -} -.fa-instagram:before { - content: '\f16d'; -} -.fa-flickr:before { - content: '\f16e'; -} -.fa-adn:before { - content: '\f170'; -} -.fa-bitbucket:before { - content: '\f171'; -} -.fa-bitbucket-square:before { - content: '\f172'; -} -.fa-tumblr:before { - content: '\f173'; -} -.fa-tumblr-square:before { - content: '\f174'; -} -.fa-long-arrow-down:before { - content: '\f175'; -} -.fa-long-arrow-up:before { - content: '\f176'; -} -.fa-long-arrow-left:before { - content: '\f177'; -} -.fa-long-arrow-right:before { - content: '\f178'; -} -.fa-apple:before { - content: '\f179'; -} -.fa-windows:before { - content: '\f17a'; -} -.fa-android:before { - content: '\f17b'; -} -.fa-linux:before { - content: '\f17c'; -} -.fa-dribbble:before { - content: '\f17d'; -} -.fa-skype:before { - content: '\f17e'; -} -.fa-foursquare:before { - content: '\f180'; -} -.fa-trello:before { - content: '\f181'; -} -.fa-female:before { - content: '\f182'; -} -.fa-male:before { - content: '\f183'; -} -.fa-gittip:before, -.fa-gratipay:before { - content: '\f184'; -} -.fa-sun-o:before { - content: '\f185'; -} -.fa-moon-o:before { - content: '\f186'; -} -.fa-archive:before { - content: '\f187'; -} -.fa-bug:before { - content: '\f188'; -} -.fa-vk:before { - content: '\f189'; -} -.fa-weibo:before { - content: '\f18a'; -} -.fa-renren:before { - content: '\f18b'; -} -.fa-pagelines:before { - content: '\f18c'; -} -.fa-stack-exchange:before { - content: '\f18d'; -} -.fa-arrow-circle-o-right:before { - content: '\f18e'; -} -.fa-arrow-circle-o-left:before { - content: '\f190'; -} -.fa-toggle-left:before, -.fa-caret-square-o-left:before { - content: '\f191'; -} -.fa-dot-circle-o:before { - content: '\f192'; -} -.fa-wheelchair:before { - content: '\f193'; -} -.fa-vimeo-square:before { - content: '\f194'; -} -.fa-turkish-lira:before, -.fa-try:before { - content: '\f195'; -} -.fa-plus-square-o:before { - content: '\f196'; -} -.fa-space-shuttle:before { - content: '\f197'; -} -.fa-slack:before { - content: '\f198'; -} -.fa-envelope-square:before { - content: '\f199'; -} -.fa-wordpress:before { - content: '\f19a'; -} -.fa-openid:before { - content: '\f19b'; -} -.fa-institution:before, -.fa-bank:before, -.fa-university:before { - content: '\f19c'; -} -.fa-mortar-board:before, -.fa-graduation-cap:before { - content: '\f19d'; -} -.fa-yahoo:before { - content: '\f19e'; -} -.fa-google:before { - content: '\f1a0'; -} -.fa-reddit:before { - content: '\f1a1'; -} -.fa-reddit-square:before { - content: '\f1a2'; -} -.fa-stumbleupon-circle:before { - content: '\f1a3'; -} -.fa-stumbleupon:before { - content: '\f1a4'; -} -.fa-delicious:before { - content: '\f1a5'; -} -.fa-digg:before { - content: '\f1a6'; -} -.fa-pied-piper:before { - content: '\f1a7'; -} -.fa-pied-piper-alt:before { - content: '\f1a8'; -} -.fa-drupal:before { - content: '\f1a9'; -} -.fa-joomla:before { - content: '\f1aa'; -} -.fa-language:before { - content: '\f1ab'; -} -.fa-fax:before { - content: '\f1ac'; -} -.fa-building:before { - content: '\f1ad'; -} -.fa-child:before { - content: '\f1ae'; -} -.fa-paw:before { - content: '\f1b0'; -} -.fa-spoon:before { - content: '\f1b1'; -} -.fa-cube:before { - content: '\f1b2'; -} -.fa-cubes:before { - content: '\f1b3'; -} -.fa-behance:before { - content: '\f1b4'; -} -.fa-behance-square:before { - content: '\f1b5'; -} -.fa-steam:before { - content: '\f1b6'; -} -.fa-steam-square:before { - content: '\f1b7'; -} -.fa-recycle:before { - content: '\f1b8'; -} -.fa-automobile:before, -.fa-car:before { - content: '\f1b9'; -} -.fa-cab:before, -.fa-taxi:before { - content: '\f1ba'; -} -.fa-tree:before { - content: '\f1bb'; -} -.fa-spotify:before { - content: '\f1bc'; -} -.fa-deviantart:before { - content: '\f1bd'; -} -.fa-soundcloud:before { - content: '\f1be'; -} -.fa-database:before { - content: '\f1c0'; -} -.fa-file-pdf-o:before { - content: '\f1c1'; -} -.fa-file-word-o:before { - content: '\f1c2'; -} -.fa-file-excel-o:before { - content: '\f1c3'; -} -.fa-file-powerpoint-o:before { - content: '\f1c4'; -} -.fa-file-photo-o:before, -.fa-file-picture-o:before, -.fa-file-image-o:before { - content: '\f1c5'; -} -.fa-file-zip-o:before, -.fa-file-archive-o:before { - content: '\f1c6'; -} -.fa-file-sound-o:before, -.fa-file-audio-o:before { - content: '\f1c7'; -} -.fa-file-movie-o:before, -.fa-file-video-o:before { - content: '\f1c8'; -} -.fa-file-code-o:before { - content: '\f1c9'; -} -.fa-vine:before { - content: '\f1ca'; -} -.fa-codepen:before { - content: '\f1cb'; -} -.fa-jsfiddle:before { - content: '\f1cc'; -} -.fa-life-bouy:before, -.fa-life-buoy:before, -.fa-life-saver:before, -.fa-support:before, -.fa-life-ring:before { - content: '\f1cd'; -} -.fa-circle-o-notch:before { - content: '\f1ce'; -} -.fa-ra:before, -.fa-rebel:before { - content: '\f1d0'; -} -.fa-ge:before, -.fa-empire:before { - content: '\f1d1'; -} -.fa-git-square:before { - content: '\f1d2'; -} -.fa-git:before { - content: '\f1d3'; -} -.fa-hacker-news:before { - content: '\f1d4'; -} -.fa-tencent-weibo:before { - content: '\f1d5'; -} -.fa-qq:before { - content: '\f1d6'; -} -.fa-wechat:before, -.fa-weixin:before { - content: '\f1d7'; -} -.fa-send:before, -.fa-paper-plane:before { - content: '\f1d8'; -} -.fa-send-o:before, -.fa-paper-plane-o:before { - content: '\f1d9'; -} -.fa-history:before { - content: '\f1da'; -} -.fa-genderless:before, -.fa-circle-thin:before { - content: '\f1db'; -} -.fa-header:before { - content: '\f1dc'; -} -.fa-paragraph:before { - content: '\f1dd'; -} -.fa-sliders:before { - content: '\f1de'; -} -.fa-share-alt:before { - content: '\f1e0'; -} -.fa-share-alt-square:before { - content: '\f1e1'; -} -.fa-bomb:before { - content: '\f1e2'; -} -.fa-soccer-ball-o:before, -.fa-futbol-o:before { - content: '\f1e3'; -} -.fa-tty:before { - content: '\f1e4'; -} -.fa-binoculars:before { - content: '\f1e5'; -} -.fa-plug:before { - content: '\f1e6'; -} -.fa-slideshare:before { - content: '\f1e7'; -} -.fa-twitch:before { - content: '\f1e8'; -} -.fa-yelp:before { - content: '\f1e9'; -} -.fa-newspaper-o:before { - content: '\f1ea'; -} -.fa-wifi:before { - content: '\f1eb'; -} -.fa-calculator:before { - content: '\f1ec'; -} -.fa-paypal:before { - content: '\f1ed'; -} -.fa-google-wallet:before { - content: '\f1ee'; -} -.fa-cc-visa:before { - content: '\f1f0'; -} -.fa-cc-mastercard:before { - content: '\f1f1'; -} -.fa-cc-discover:before { - content: '\f1f2'; -} -.fa-cc-amex:before { - content: '\f1f3'; -} -.fa-cc-paypal:before { - content: '\f1f4'; -} -.fa-cc-stripe:before { - content: '\f1f5'; -} -.fa-bell-slash:before { - content: '\f1f6'; -} -.fa-bell-slash-o:before { - content: '\f1f7'; -} -.fa-trash:before { - content: '\f1f8'; -} -.fa-copyright:before { - content: '\f1f9'; -} -.fa-at:before { - content: '\f1fa'; -} -.fa-eyedropper:before { - content: '\f1fb'; -} -.fa-paint-brush:before { - content: '\f1fc'; -} -.fa-birthday-cake:before { - content: '\f1fd'; -} -.fa-area-chart:before { - content: '\f1fe'; -} -.fa-pie-chart:before { - content: '\f200'; -} -.fa-line-chart:before { - content: '\f201'; -} -.fa-lastfm:before { - content: '\f202'; -} -.fa-lastfm-square:before { - content: '\f203'; -} -.fa-toggle-off:before { - content: '\f204'; -} -.fa-toggle-on:before { - content: '\f205'; -} -.fa-bicycle:before { - content: '\f206'; -} -.fa-bus:before { - content: '\f207'; -} -.fa-ioxhost:before { - content: '\f208'; -} -.fa-angellist:before { - content: '\f209'; -} -.fa-cc:before { - content: '\f20a'; -} -.fa-shekel:before, -.fa-sheqel:before, -.fa-ils:before { - content: '\f20b'; -} -.fa-meanpath:before { - content: '\f20c'; -} -.fa-buysellads:before { - content: '\f20d'; -} -.fa-connectdevelop:before { - content: '\f20e'; -} -.fa-dashcube:before { - content: '\f210'; -} -.fa-forumbee:before { - content: '\f211'; -} -.fa-leanpub:before { - content: '\f212'; -} -.fa-sellsy:before { - content: '\f213'; -} -.fa-shirtsinbulk:before { - content: '\f214'; -} -.fa-simplybuilt:before { - content: '\f215'; -} -.fa-skyatlas:before { - content: '\f216'; -} -.fa-cart-plus:before { - content: '\f217'; -} -.fa-cart-arrow-down:before { - content: '\f218'; -} -.fa-diamond:before { - content: '\f219'; -} -.fa-ship:before { - content: '\f21a'; -} -.fa-user-secret:before { - content: '\f21b'; -} -.fa-motorcycle:before { - content: '\f21c'; -} -.fa-street-view:before { - content: '\f21d'; -} -.fa-heartbeat:before { - content: '\f21e'; -} -.fa-venus:before { - content: '\f221'; -} -.fa-mars:before { - content: '\f222'; -} -.fa-mercury:before { - content: '\f223'; -} -.fa-transgender:before { - content: '\f224'; -} -.fa-transgender-alt:before { - content: '\f225'; -} -.fa-venus-double:before { - content: '\f226'; -} -.fa-mars-double:before { - content: '\f227'; -} -.fa-venus-mars:before { - content: '\f228'; -} -.fa-mars-stroke:before { - content: '\f229'; -} -.fa-mars-stroke-v:before { - content: '\f22a'; -} -.fa-mars-stroke-h:before { - content: '\f22b'; -} -.fa-neuter:before { - content: '\f22c'; -} -.fa-facebook-official:before { - content: '\f230'; -} -.fa-pinterest-p:before { - content: '\f231'; -} -.fa-whatsapp:before { - content: '\f232'; -} -.fa-server:before { - content: '\f233'; -} -.fa-user-plus:before { - content: '\f234'; -} -.fa-user-times:before { - content: '\f235'; -} -.fa-hotel:before, -.fa-bed:before { - content: '\f236'; -} -.fa-viacoin:before { - content: '\f237'; -} -.fa-train:before { - content: '\f238'; -} -.fa-subway:before { - content: '\f239'; -} -.fa-medium:before { - content: '\f23a'; -} diff --git a/webapp/sass/vendors/_module.scss b/webapp/sass/vendors/_module.scss deleted file mode 100644 index ed8a124a2..000000000 --- a/webapp/sass/vendors/_module.scss +++ /dev/null @@ -1,4 +0,0 @@ -// Only for combining all the files in this folder -@import 'perfect-scrollbar'; -@import 'font-awesome'; -@import 'colorpicker'; diff --git a/webapp/sass/vendors/_perfect-scrollbar.scss b/webapp/sass/vendors/_perfect-scrollbar.scss deleted file mode 100755 index 212a22687..000000000 --- a/webapp/sass/vendors/_perfect-scrollbar.scss +++ /dev/null @@ -1,141 +0,0 @@ -@charset 'UTF-8'; - -.ps-container { - overflow: hidden !important; -} -.ps-container.ps-active-x > .ps-scrollbar-x-rail, -.ps-container.ps-active-y > .ps-scrollbar-y-rail { - display: block; -} - -.ps-container.ps-in-scrolling.ps-x > .ps-scrollbar-x-rail { - background-color: #eee; - opacity: .9; - -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=90)'; - filter: alpha(opacity=90); -} -.ps-container.ps-in-scrolling.ps-x > .ps-scrollbar-x-rail > .ps-scrollbar-x { - background-color: #999; -} -.ps-container.ps-in-scrolling.ps-y > .ps-scrollbar-y-rail { - background-color: #eee; - opacity: .9; - -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=90)'; - filter: alpha(opacity=90); -} -.ps-container.ps-in-scrolling.ps-y > .ps-scrollbar-y-rail > .ps-scrollbar-y { - background-color: #999; -} -.ps-container > .ps-scrollbar-x-rail { - display: none; - position: absolute; - /* please don't change 'position' */ - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - -ms-border-radius: 4px; - border-radius: 4px; - opacity: 0; - -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=0)'; - filter: alpha(opacity=0); - -webkit-transition: background-color .2s linear, opacity .2s linear; - -moz-transition: background-color .2s linear, opacity .2s linear; - -o-transition: background-color .2s linear, opacity .2s linear; - transition: background-color .2s linear, opacity .2s linear; - bottom: 3px; - /* there must be 'bottom' for ps-scrollbar-x-rail */ - height: 8px; -} -.ps-container > .ps-scrollbar-x-rail > .ps-scrollbar-x { - position: absolute; - /* please don't change 'position' */ - background-color: #aaa; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - -ms-border-radius: 4px; - border-radius: 4px; - -webkit-transition: background-color .2s linear; - -moz-transition: background-color .2s linear; - -o-transition: background-color .2s linear; - transition: background-color .2s linear; - bottom: 0; - /* there must be 'bottom' for ps-scrollbar-x */ - height: 8px; -} -.ps-container > .ps-scrollbar-y-rail { - display: none; - position: absolute; - /* please don't change 'position' */ - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - -ms-border-radius: 4px; - border-radius: 4px; - opacity: 0; - -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=0)'; - filter: alpha(opacity=0); - -webkit-transition: background-color .2s linear, opacity .2s linear; - -moz-transition: background-color .2s linear, opacity .2s linear; - -o-transition: background-color .2s linear, opacity .2s linear; - transition: background-color .2s linear, opacity .2s linear; - right: 3px; - /* there must be 'right' for ps-scrollbar-y-rail */ - width: 8px; -} -.ps-container > .ps-scrollbar-y-rail > .ps-scrollbar-y { - position: absolute; - /* please don't change 'position' */ - background-color: #aaa; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - -ms-border-radius: 4px; - border-radius: 4px; - -webkit-transition: background-color .2s linear; - -moz-transition: background-color .2s linear; - -o-transition: background-color .2s linear; - transition: background-color .2s linear; - right: 0; - /* there must be 'right' for ps-scrollbar-y */ - width: 8px; -} - -.ps-container:hover.ps-in-scrolling.ps-x > .ps-scrollbar-x-rail { - background-color: #eee; - opacity: .9; - -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=90)'; - filter: alpha(opacity=90); -} -.ps-container:hover.ps-in-scrolling.ps-x > .ps-scrollbar-x-rail > .ps-scrollbar-x { - background-color: #999; -} -.ps-container:hover.ps-in-scrolling.ps-y > .ps-scrollbar-y-rail { - background-color: #eee; - opacity: .9; - -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=90)'; - filter: alpha(opacity=90); -} -.ps-container:hover.ps-in-scrolling.ps-y > .ps-scrollbar-y-rail > .ps-scrollbar-y { - background-color: #999; -} -.ps-container:hover > .ps-scrollbar-x-rail, -.ps-container:hover > .ps-scrollbar-y-rail { - opacity: .6; - -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=60)'; - filter: alpha(opacity=60); -} -.ps-container:hover > .ps-scrollbar-x-rail:hover { - background-color: #eee; - opacity: .9; - -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=90)'; - filter: alpha(opacity=90); -} -.ps-container:hover > .ps-scrollbar-x-rail:hover > .ps-scrollbar-x { - background-color: #999; -} -.ps-container:hover > .ps-scrollbar-y-rail:hover { - background-color: #eee; - opacity: .9; - -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=90)'; - filter: alpha(opacity=90); -} -.ps-container:hover > .ps-scrollbar-y-rail:hover > .ps-scrollbar-y { - background-color: #999; -} diff --git a/webapp/stores/browser_store.jsx b/webapp/stores/browser_store.jsx index bba146e38..d605aac80 100644 --- a/webapp/stores/browser_store.jsx +++ b/webapp/stores/browser_store.jsx @@ -8,7 +8,7 @@ function getPrefix() { return global.window.mm_current_user_id + '_'; } - console.log('BrowserStore tried to operate without user present'); //eslint-disable-line no-console + console.warn('BrowserStore tried to operate without user present'); //eslint-disable-line no-console return 'unknown_'; } @@ -144,18 +144,14 @@ class BrowserStoreClass { * Signature for action is action(key, value) */ actionOnGlobalItemsWithPrefix(prefix, action) { - var globalPrefix = getPrefix(); - var globalPrefixiLen = globalPrefix.length; - var storage = sessionStorage; if (this.isLocalStorageSupported()) { storage = localStorage; } for (var key in storage) { - if (key.lastIndexOf(globalPrefix + prefix, 0) === 0) { - var userkey = key.substring(globalPrefixiLen); - action(userkey, this.getGlobalItem(key)); + if (key.lastIndexOf(prefix, 0) === 0) { + action(key, this.getGlobalItem(key)); } } } diff --git a/webapp/stores/error_store.jsx b/webapp/stores/error_store.jsx index 7c695a335..715029185 100644 --- a/webapp/stores/error_store.jsx +++ b/webapp/stores/error_store.jsx @@ -59,6 +59,7 @@ class ErrorStoreClass extends EventEmitter { clearLastError() { BrowserStore.removeGlobalItem('last_error'); BrowserStore.removeGlobalItem('last_error_conn'); + this.emitChange(); } } diff --git a/webapp/stores/file_store.jsx b/webapp/stores/file_store.jsx index 2628685cc..2692e6959 100644 --- a/webapp/stores/file_store.jsx +++ b/webapp/stores/file_store.jsx @@ -13,10 +13,6 @@ class FileStore extends EventEmitter { constructor() { super(); - this.addChangeListener = this.addChangeListener.bind(this); - this.removeChangeListener = this.removeChangeListener.bind(this); - this.emitChange = this.emitChange.bind(this); - this.handleEventPayload = this.handleEventPayload.bind(this); this.dispatchToken = AppDispatcher.register(this.handleEventPayload); diff --git a/webapp/stores/integration_store.jsx b/webapp/stores/integration_store.jsx new file mode 100644 index 000000000..abd7e3558 --- /dev/null +++ b/webapp/stores/integration_store.jsx @@ -0,0 +1,134 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import Constants from 'utils/constants.jsx'; +import EventEmitter from 'events'; + +const ActionTypes = Constants.ActionTypes; + +const CHANGE_EVENT = 'changed'; + +class IntegrationStore extends EventEmitter { + constructor() { + super(); + + this.dispatchToken = AppDispatcher.register(this.handleEventPayload.bind(this)); + + this.incomingWebhooks = []; + this.receivedIncomingWebhooks = false; + + this.outgoingWebhooks = []; + this.receivedOutgoingWebhooks = false; + } + + addChangeListener(callback) { + this.on(CHANGE_EVENT, callback); + } + + removeChangeListener(callback) { + this.removeListener(CHANGE_EVENT, callback); + } + + emitChange() { + this.emit(CHANGE_EVENT); + } + + hasReceivedIncomingWebhooks() { + return this.receivedIncomingWebhooks; + } + + getIncomingWebhooks() { + return this.incomingWebhooks; + } + + setIncomingWebhooks(incomingWebhooks) { + this.incomingWebhooks = incomingWebhooks; + this.receivedIncomingWebhooks = true; + } + + addIncomingWebhook(incomingWebhook) { + this.incomingWebhooks.push(incomingWebhook); + } + + removeIncomingWebhook(id) { + for (let i = 0; i < this.incomingWebhooks.length; i++) { + if (this.incomingWebhooks[i].id === id) { + this.incomingWebhooks.splice(i, 1); + break; + } + } + } + + hasReceivedOutgoingWebhooks() { + return this.receivedIncomingWebhooks; + } + + getOutgoingWebhooks() { + return this.outgoingWebhooks; + } + + setOutgoingWebhooks(outgoingWebhooks) { + this.outgoingWebhooks = outgoingWebhooks; + this.receivedOutgoingWebhooks = true; + } + + addOutgoingWebhook(outgoingWebhook) { + this.outgoingWebhooks.push(outgoingWebhook); + } + + updateOutgoingWebhook(outgoingWebhook) { + for (let i = 0; i < this.outgoingWebhooks.length; i++) { + if (this.outgoingWebhooks[i].id === outgoingWebhook.id) { + this.outgoingWebhooks[i] = outgoingWebhook; + break; + } + } + } + + removeOutgoingWebhook(id) { + for (let i = 0; i < this.outgoingWebhooks.length; i++) { + if (this.outgoingWebhooks[i].id === id) { + this.outgoingWebhooks.splice(i, 1); + break; + } + } + } + + handleEventPayload(payload) { + const action = payload.action; + + switch (action.type) { + case ActionTypes.RECEIVED_INCOMING_WEBHOOKS: + this.setIncomingWebhooks(action.incomingWebhooks); + this.emitChange(); + break; + case ActionTypes.RECEIVED_INCOMING_WEBHOOK: + this.addIncomingWebhook(action.incomingWebhook); + this.emitChange(); + break; + case ActionTypes.REMOVED_INCOMING_WEBHOOK: + this.removeIncomingWebhook(action.id); + this.emitChange(); + break; + case ActionTypes.RECEIVED_OUTGOING_WEBHOOKS: + this.setOutgoingWebhooks(action.outgoingWebhooks); + this.emitChange(); + break; + case ActionTypes.RECEIVED_OUTGOING_WEBHOOK: + this.addOutgoingWebhook(action.outgoingWebhook); + this.emitChange(); + break; + case ActionTypes.UPDATED_OUTGOING_WEBHOOK: + this.updateOutgoingWebhook(action.outgoingWebhook); + this.emitChange(); + break; + case ActionTypes.REMOVED_OUTGOING_WEBHOOK: + this.removeOutgoingWebhook(action.id); + this.emitChange(); + break; + } + } +} + +export default new IntegrationStore(); diff --git a/webapp/stores/notificaiton_store.jsx b/webapp/stores/notification_store.jsx index 70caffeb6..6722af281 100644 --- a/webapp/stores/notificaiton_store.jsx +++ b/webapp/stores/notification_store.jsx @@ -89,7 +89,7 @@ NotificationStore.dispatchToken = AppDispatcher.register((payload) => { switch (action.type) { case ActionTypes.RECEIVED_POST: - NotificationStore.handleRecievedPost(action.post, action.webspcketMessageProps); + NotificationStore.handleRecievedPost(action.post, action.websocketMessageProps); NotificationStore.emitChange(); break; } diff --git a/webapp/stores/post_store.jsx b/webapp/stores/post_store.jsx index 903085760..3f2f75796 100644 --- a/webapp/stores/post_store.jsx +++ b/webapp/stores/post_store.jsx @@ -96,7 +96,7 @@ class PostStoreClass extends EventEmitter { let post = null; if (posts.posts.hasOwnProperty(postId)) { - post = Object.assign({}, posts.posts[postId]); + post = posts.posts[postId]; } return post; @@ -104,7 +104,7 @@ class PostStoreClass extends EventEmitter { getAllPosts(id) { if (this.postsInfo.hasOwnProperty(id)) { - return Object.assign({}, this.postsInfo[id].postList); + return this.postsInfo[id].postList; } return null; @@ -406,7 +406,7 @@ class PostStoreClass extends EventEmitter { let posts; let pendingPosts; for (const k in this.postsInfo) { - if (this.postsInfo[k].postList.posts.hasOwnProperty(this.selectedPostId)) { + if (this.postsInfo[k].postList && this.postsInfo[k].postList.posts.hasOwnProperty(this.selectedPostId)) { posts = this.postsInfo[k].postList.posts; if (this.postsInfo[k].pendingPosts != null) { pendingPosts = this.postsInfo[k].pendingPosts.posts; @@ -495,7 +495,7 @@ class PostStoreClass extends EventEmitter { BrowserStore.actionOnGlobalItemsWithPrefix('draft_', (key, value) => { if (value) { value.uploadsInProgress = []; - BrowserStore.setItem(key, value); + BrowserStore.setGlobalItem(key, value); } }); } @@ -503,7 +503,7 @@ class PostStoreClass extends EventEmitter { BrowserStore.actionOnGlobalItemsWithPrefix('comment_draft_', (key, value) => { if (value) { value.uploadsInProgress = []; - BrowserStore.setItem(key, value); + BrowserStore.setGlobalItem(key, value); } }); } @@ -531,8 +531,8 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => { switch (action.type) { case ActionTypes.RECEIVED_POSTS: { const id = PostStore.currentFocusedPostId == null ? action.id : PostStore.currentFocusedPostId; - PostStore.checkBounds(id, action.numRequested, makePostListNonNull(action.post_list), action.before); PostStore.storePosts(id, makePostListNonNull(action.post_list)); + PostStore.checkBounds(id, action.numRequested, makePostListNonNull(action.post_list), action.before); PostStore.emitChange(); break; } diff --git a/webapp/stores/search_store.jsx b/webapp/stores/search_store.jsx index acaa9e52f..dc08ca3a6 100644 --- a/webapp/stores/search_store.jsx +++ b/webapp/stores/search_store.jsx @@ -16,7 +16,7 @@ class SearchStoreClass extends EventEmitter { constructor() { super(); - this.searchResults = {}; + this.searchResults = null; this.isMentionSearch = false; this.searchTerm = ''; } diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx index 6140fd9e0..cc19baa7e 100644 --- a/webapp/utils/async_client.jsx +++ b/webapp/utils/async_client.jsx @@ -1121,3 +1121,140 @@ export function getRecentAndNewUsersAnalytics(teamId) { } ); } + +export function listIncomingHooks() { + if (isCallInProgress('listIncomingHooks')) { + return; + } + + callTracker.listIncomingHooks = utils.getTimestamp(); + + client.listIncomingHooks( + (data) => { + callTracker.listIncomingHooks = 0; + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_INCOMING_WEBHOOKS, + incomingWebhooks: data + }); + }, + (err) => { + callTracker.listIncomingHooks = 0; + dispatchError(err, 'getIncomingHooks'); + } + ); +} + +export function listOutgoingHooks() { + if (isCallInProgress('listOutgoingHooks')) { + return; + } + + callTracker.listOutgoingHooks = utils.getTimestamp(); + + client.listOutgoingHooks( + (data) => { + callTracker.listOutgoingHooks = 0; + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_OUTGOING_WEBHOOKS, + outgoingWebhooks: data + }); + }, + (err) => { + callTracker.listOutgoingHooks = 0; + dispatchError(err, 'getOutgoingHooks'); + } + ); +} + +export function addIncomingHook(hook, success, error) { + client.addIncomingHook( + hook, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_INCOMING_WEBHOOK, + incomingWebhook: data + }); + + if (success) { + success(); + } + }, + (err) => { + dispatchError(err, 'addIncomingHook'); + + if (error) { + error(err); + } + } + ); +} + +export function addOutgoingHook(hook, success, error) { + client.addOutgoingHook( + hook, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_OUTGOING_WEBHOOK, + outgoingWebhook: data + }); + + if (success) { + success(); + } + }, + (err) => { + dispatchError(err, 'addOutgoingHook'); + + if (error) { + error(err); + } + } + ); +} + +export function deleteIncomingHook(id) { + client.deleteIncomingHook( + {id}, + () => { + AppDispatcher.handleServerAction({ + type: ActionTypes.REMOVED_INCOMING_WEBHOOK, + id + }); + }, + (err) => { + dispatchError(err, 'deleteIncomingHook'); + } + ); +} + +export function deleteOutgoingHook(id) { + client.deleteOutgoingHook( + {id}, + () => { + AppDispatcher.handleServerAction({ + type: ActionTypes.REMOVED_OUTGOING_WEBHOOK, + id + }); + }, + (err) => { + dispatchError(err, 'deleteOutgoingHook'); + } + ); +} + +export function regenOutgoingHookToken(id) { + client.regenOutgoingHookToken( + {id}, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.UPDATED_OUTGOING_WEBHOOK, + outgoingWebhook: data + }); + }, + (err) => { + dispatchError(err, 'regenOutgoingHookToken'); + } + ); +} diff --git a/webapp/utils/client.jsx b/webapp/utils/client.jsx index d42767d31..854aa31dc 100644 --- a/webapp/utils/client.jsx +++ b/webapp/utils/client.jsx @@ -50,12 +50,8 @@ function handleError(methodName, xhr, status, err) { track('api', 'api_weberror', methodName, 'message', msg); if (xhr.status === 401) { - if (window.location.href.indexOf('/channels') === 0) { - browserHistory.push('/login?extra=expired&redirect=' + encodeURIComponent(window.location.pathname + window.location.search)); - } else { - var teamURL = window.location.pathname.split('/channels')[0]; - browserHistory.push(teamURL + '/login?extra=expired&redirect=' + encodeURIComponent(window.location.pathname + window.location.search)); - } + const team = window.location.pathname.split('/')[1]; + browserHistory.push('/' + team + '/login?extra=expired&redirect=' + encodeURIComponent(window.location.pathname + window.location.search)); } return e; @@ -337,13 +333,28 @@ export function logout(success, error) { }); } -export function loginByEmail(name, email, password, success, error) { +export function checkMfa(method, team, loginId, success, error) { + $.ajax({ + url: '/api/v1/users/mfa', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify({method, team_name: team, login_id: loginId}), + success, + error: function onError(xhr, status, err) { + var e = handleError('checkMfa', xhr, status, err); + error(e); + } + }); +} + +export function loginByEmail(name, email, password, token, success, error) { $.ajax({ url: '/api/v1/users/login', dataType: 'json', contentType: 'application/json', type: 'POST', - data: JSON.stringify({name, email, password}), + data: JSON.stringify({name, email, password, token}), success: function onSuccess(data, textStatus, xhr) { track('api', 'api_users_login_success', data.team_id, 'email', data.email); sessionStorage.removeItem(data.id + '_last_error'); @@ -381,13 +392,13 @@ export function loginByUsername(name, username, password, success, error) { }); } -export function loginByLdap(teamName, id, password, success, error) { +export function loginByLdap(teamName, id, password, token, success, error) { $.ajax({ url: '/api/v1/users/login_ldap', dataType: 'json', contentType: 'application/json', type: 'POST', - data: JSON.stringify({teamName, id, password}), + data: JSON.stringify({teamName, id, password, token}), success: function onSuccess(data, textStatus, xhr) { track('api', 'api_users_loginLdap_success', data.team_id, 'id', id); sessionStorage.removeItem(data.id + '_last_error'); @@ -1712,3 +1723,18 @@ export function resendVerification(success, error, teamName, email) { } }); } + +export function updateMfa(data, success, error) { + $.ajax({ + url: '/api/v1/users/update_mfa', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(data), + success, + error: (xhr, status, err) => { + var e = handleError('updateMfa', xhr, status, err); + error(e); + } + }); +} diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index bcd2fadb9..d01163b31 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -14,6 +14,7 @@ import patchIcon from 'images/icons/patch.png'; import genericIcon from 'images/icons/generic.png'; import logoImage from 'images/logo_compact.png'; +import logoWebhook from 'images/webhook_icon.jpg'; import solarizedDarkCSS from '!!file?name=files/code_themes/[hash].[ext]!highlight.js/styles/solarized-dark.css'; import solarizedDarkIcon from 'images/themes/code_themes/solarized-dark.png'; @@ -68,6 +69,15 @@ export default { RECEIVED_PREFERENCE: null, RECEIVED_PREFERENCES: null, RECEIVED_FILE_INFO: null, + RECEIVED_ANALYTICS: null, + + RECEIVED_INCOMING_WEBHOOKS: null, + RECEIVED_INCOMING_WEBHOOK: null, + REMOVED_INCOMING_WEBHOOK: null, + RECEIVED_OUTGOING_WEBHOOKS: null, + RECEIVED_OUTGOING_WEBHOOK: null, + UPDATED_OUTGOING_WEBHOOK: null, + REMOVED_OUTGOING_WEBHOOK: null, RECEIVED_MSG: null, @@ -182,11 +192,14 @@ export default { MOBILE_VIDEO_WIDTH: 480, MOBILE_VIDEO_HEIGHT: 360, DEFAULT_CHANNEL: 'town-square', + DEFAULT_CHANNEL_UI_NAME: 'Town Square', OFFTOPIC_CHANNEL: 'off-topic', + OFFTOPIC_CHANNEL_UI_NAME: 'Off-Topic', GITLAB_SERVICE: 'gitlab', GOOGLE_SERVICE: 'google', - LDAP_SERVICE: 'ldap', EMAIL_SERVICE: 'email', + LDAP_SERVICE: 'ldap', + USERNAME_SERVICE: 'username', SIGNIN_CHANGE: 'signin_change', SIGNIN_VERIFIED: 'verified', SESSION_EXPIRED: 'expired', @@ -235,6 +248,7 @@ export default { OPEN_TEAM: 'O', MAX_POST_LEN: 4000, EMOJI_SIZE: 16, + MATTERMOST_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'viewBox='0 0 500 500' style='enable-background:new 0 0 500 500;' xml:space='preserve'> <style type='text/css'> .st0{fill-rule:evenodd;clip-rule:evenodd;fill:#222222;} </style> <g id='XMLID_1_'> <g id='XMLID_3_'> <path id='XMLID_4_' class='st0' d='M396.9,47.7l2.6,53.1c43,47.5,60,114.8,38.6,178.1c-32,94.4-137.4,144.1-235.4,110.9 S51.1,253.1,83,158.7C104.5,95.2,159.2,52,222.5,40.5l34.2-40.4C150-2.8,49.3,63.4,13.3,169.9C-31,300.6,39.1,442.5,169.9,486.7 s272.6-25.8,316.9-156.6C522.7,223.9,483.1,110.3,396.9,47.7z'/> </g> <path id='XMLID_2_' class='st0' d='M335.6,204.3l-1.8-74.2l-1.5-42.7l-1-37c0,0,0.2-17.8-0.4-22c-0.1-0.9-0.4-1.6-0.7-2.2 c0-0.1-0.1-0.2-0.1-0.3c0-0.1-0.1-0.2-0.1-0.2c-0.7-1.2-1.8-2.1-3.1-2.6c-1.4-0.5-2.9-0.4-4.2,0.2c0,0-0.1,0-0.1,0 c-0.2,0.1-0.3,0.1-0.4,0.2c-0.6,0.3-1.2,0.7-1.8,1.3c-3,3-13.7,17.2-13.7,17.2l-23.2,28.8l-27.1,33l-46.5,57.8 c0,0-21.3,26.6-16.6,59.4s29.1,48.7,48,55.1c18.9,6.4,48,8.5,71.6-14.7C336.4,238.4,335.6,204.3,335.6,204.3z'/> </g> </svg>", ONLINE_ICON_SVG: "<svg version='1.1'id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:cc='http://creativecommons.org/ns#' inkscape:version='0.48.4 r9939' sodipodi:docname='TRASH_1_4.svg'xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='-243 245 12 12'style='enable-background:new -243 245 12 12;' xml:space='preserve'> <sodipodi:namedview inkscape:cx='26.358185' inkscape:zoom='1.18' bordercolor='#666666' pagecolor='#ffffff' borderopacity='1' objecttolerance='10' inkscape:cy='139.7898' gridtolerance='10' guidetolerance='10' showgrid='false' showguides='true' id='namedview6' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:guide-bbox='true' inkscape:window-width='1366' inkscape:current-layer='Layer_1' inkscape:window-height='705' inkscape:window-y='-8' inkscape:window-maximized='1' inkscape:window-x='-8'> <sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide> <sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide> </sodipodi:namedview> <g> <path class='online--icon' d='M-236,250.5C-236,250.5-236,250.5-236,250.5C-236,250.5-236,250.5-236,250.5C-236,250.5-236,250.5-236,250.5z'/> <ellipse class='online--icon' cx='-238.5' cy='248' rx='2.5' ry='2.5'/> </g> <path class='online--icon' d='M-238.9,253.8c0-0.4,0.1-0.9,0.2-1.3c-2.2-0.2-2.2-2-2.2-2s-1,0.1-1.2,0.5c-0.4,0.6-0.6,1.7-0.7,2.5c0,0.1-0.1,0.5,0,0.6 c0.2,1.3,2.2,2.3,4.4,2.4c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0C-238.7,255.7-238.9,254.8-238.9,253.8z'/> <g> <g> <path class='online--icon' d='M-232.3,250.1l1.3,1.3c0,0,0,0.1,0,0.1l-4.1,4.1c0,0,0,0-0.1,0c0,0,0,0,0,0l-2.7-2.7c0,0,0-0.1,0-0.1l1.2-1.2 c0,0,0.1,0,0.1,0l1.4,1.4l2.9-2.9C-232.4,250.1-232.3,250.1-232.3,250.1z'/> </g> </g> </svg>", AWAY_ICON_SVG: "<svg version='1.1'id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:cc='http://creativecommons.org/ns#' inkscape:version='0.48.4 r9939' sodipodi:docname='TRASH_1_4.svg'xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='-299 391 12 12'style='enable-background:new -299 391 12 12;' xml:space='preserve'> <sodipodi:namedview inkscape:cx='26.358185' inkscape:zoom='1.18' bordercolor='#666666' pagecolor='#ffffff' borderopacity='1' objecttolerance='10' inkscape:cy='139.7898' gridtolerance='10' guidetolerance='10' showgrid='false' showguides='true' id='namedview6' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:guide-bbox='true' inkscape:window-width='1366' inkscape:current-layer='Layer_1' inkscape:window-height='705' inkscape:window-y='-8' inkscape:window-maximized='1' inkscape:window-x='-8'> <sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide> <sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide> </sodipodi:namedview> <g> <ellipse class='away--icon' cx='-294.6' cy='394' rx='2.5' ry='2.5'/> <path class='away--icon' d='M-293.8,399.4c0-0.4,0.1-0.7,0.2-1c-0.3,0.1-0.6,0.2-1,0.2c-2.5,0-2.5-2-2.5-2s-1,0.1-1.2,0.5c-0.4,0.6-0.6,1.7-0.7,2.5 c0,0.1-0.1,0.5,0,0.6c0.2,1.3,2.2,2.3,4.4,2.4c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0c0.7,0,1.4-0.1,2-0.3 C-293.3,401.5-293.8,400.5-293.8,399.4z'/> </g> <path class='away--icon' d='M-287,400c0,0.1-0.1,0.1-0.1,0.1l-4.9,0c-0.1,0-0.1-0.1-0.1-0.1v-1.6c0-0.1,0.1-0.1,0.1-0.1l4.9,0c0.1,0,0.1,0.1,0.1,0.1 V400z'/> </svg>", OFFLINE_ICON_SVG: "<svg version='1.1'id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:cc='http://creativecommons.org/ns#' inkscape:version='0.48.4 r9939' sodipodi:docname='TRASH_1_4.svg'xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='-299 391 12 12'style='enable-background:new -299 391 12 12;' xml:space='preserve'> <sodipodi:namedview inkscape:cx='26.358185' inkscape:zoom='1.18' bordercolor='#666666' pagecolor='#ffffff' borderopacity='1' objecttolerance='10' inkscape:cy='139.7898' gridtolerance='10' guidetolerance='10' showgrid='false' showguides='true' id='namedview6' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:guide-bbox='true' inkscape:window-width='1366' inkscape:current-layer='Layer_1' inkscape:window-height='705' inkscape:window-y='-8' inkscape:window-maximized='1' inkscape:window-x='-8'> <sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide> <sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide> </sodipodi:namedview> <g> <g> <ellipse class='offline--icon' cx='-294.5' cy='394' rx='2.5' ry='2.5'/> <path class='offline--icon' d='M-294.3,399.7c0-0.4,0.1-0.8,0.2-1.2c-0.1,0-0.2,0-0.4,0c-2.5,0-2.5-2-2.5-2s-1,0.1-1.2,0.5c-0.4,0.6-0.6,1.7-0.7,2.5 c0,0.1-0.1,0.5,0,0.6c0.2,1.3,2.2,2.3,4.4,2.4h0.1h0.1c0.3,0,0.7,0,1-0.1C-293.9,401.6-294.3,400.7-294.3,399.7z'/> </g> </g> <g> <path class='offline--icon' d='M-288.9,399.4l1.8-1.8c0.1-0.1,0.1-0.3,0-0.3l-0.7-0.7c-0.1-0.1-0.3-0.1-0.3,0l-1.8,1.8l-1.8-1.8c-0.1-0.1-0.3-0.1-0.3,0 l-0.7,0.7c-0.1,0.1-0.1,0.3,0,0.3l1.8,1.8l-1.8,1.8c-0.1,0.1-0.1,0.3,0,0.3l0.7,0.7c0.1,0.1,0.3,0.1,0.3,0l1.8-1.8l1.8,1.8 c0.1,0.1,0.3,0.1,0.3,0l0.7-0.7c0.1-0.1,0.1-0.3,0-0.3L-288.9,399.4z'/> </g> </svg>", @@ -607,5 +621,6 @@ export default { MAX_PASSWORD_LENGTH: 50, TIME_SINCE_UPDATE_INTERVAL: 30000, MIN_HASHTAG_LINK_LENGTH: 3, - EMOJI_PATH: '/static/emoji' + EMOJI_PATH: '/static/emoji', + DEFAULT_WEBHOOK_LOGO: logoWebhook }; diff --git a/webapp/utils/delayed_action.jsx b/webapp/utils/delayed_action.jsx index 4f6239ad0..c3b164733 100644 --- a/webapp/utils/delayed_action.jsx +++ b/webapp/utils/delayed_action.jsx @@ -24,4 +24,8 @@ export default class DelayedAction { this.timer = window.setTimeout(this.fire, timeout); } + + cancel() { + window.clearTimeout(this.timer); + } } diff --git a/webapp/utils/emoji.json b/webapp/utils/emoji.json index 1ccb129f2..c01f5b679 100644 --- a/webapp/utils/emoji.json +++ b/webapp/utils/emoji.json @@ -8124,6 +8124,64 @@ ] } , { + "emoji": "π¨π¦" + , "description": "regional indicator symbol letter c + regional indicator symbol letter a" + , "aliases": [ + "ca", + "eh" + ] + , "tags": [ + "canada" + ] + } +, { + "emoji": "π΅π°" + , "description": "regional indicator symbol letter p + regional indicator symbol letter k" + , "aliases": [ + "pk" + ] + , "tags": [ + "pakistan" + ] + } +, { + "emoji": "πΏπ¦" + , "description": "regional indicator symbol letter z + regional indicator symbol letter a" + , "aliases": [ + "za" + ] + , "tags": [ + "south_africa" + ] + } +, { + "emoji": "π" + , "description": "slightly smiling face" + , "aliases": [ + "slightly_smiling_face" + ] + , "tags": [ + ] + } +, { + "emoji": "π" + , "description": "slightly frowning face" + , "aliases": [ + "slightly_frowning_face" + ] + , "tags": [ + ] + } +, { + "emoji": "π" + , "description": "upside-down face" + , "aliases": [ + "upside_down_face" + ] + , "tags": [ + ] + } +, { "aliases": [ "basecamp" ] diff --git a/webapp/utils/emoticons.jsx b/webapp/utils/emoticons.jsx index d3afe372a..86f7a5b7b 100644 --- a/webapp/utils/emoticons.jsx +++ b/webapp/utils/emoticons.jsx @@ -7,7 +7,7 @@ import Constants from './constants.jsx'; import emojis from './emoji.json'; const emoticonPatterns = { - smile: /(^|\s)(:-?\))(?=$|\s)/g, // :) + slightly_smiling_face: /(^|\s)(:-?\))(?=$|\s)/g, // :) wink: /(^|\s)(;-?\))(?=$|\s)/g, // ;) open_mouth: /(^|\s)(:o)(?=$|\s)/gi, // :o scream: /(^|\s)(:-o)(?=$|\s)/gi, // :-o @@ -16,7 +16,7 @@ const emoticonPatterns = { stuck_out_tongue_closed_eyes: /(^|\s)(x-d)(?=$|\s)/gi, // x-d stuck_out_tongue: /(^|\s)(:-?p)(?=$|\s)/gi, // :p rage: /(^|\s)(:-?[\[@])(?=$|\s)/g, // :@ - frowning: /(^|\s)(:-?\()(?=$|\s)/g, // :( + slightly_frowning_face: /(^|\s)(:-?\()(?=$|\s)/g, // :( cry: /(^|\s)(:['β]-?\(|:'\(|:'\()(?=$|\s)/g, // :`( confused: /(^|\s)(:-?\/)(?=$|\s)/g, // :/ confounded: /(^|\s)(:-?s)(?=$|\s)/gi, // :s diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx index ac12edb82..9b03ef32a 100644 --- a/webapp/utils/utils.jsx +++ b/webapp/utils/utils.jsx @@ -14,7 +14,6 @@ var ActionTypes = Constants.ActionTypes; import * as Client from './client.jsx'; import * as AsyncClient from './async_client.jsx'; import * as client from './client.jsx'; -import Autolinker from 'autolinker'; import React from 'react'; import {browserHistory} from 'react-router'; @@ -169,7 +168,7 @@ export function notifyMe(title, body, channel) { notification.onclick = () => { window.focus(); if (channel) { - switchChannel(channel); + GlobalActions.emitChannelClickEvent(channel); } else { browserHistory.push(TeamStore.getCurrentTeamUrl() + '/channels/town-square'); } @@ -314,14 +313,8 @@ export function getTimestamp() { } // extracts links not styled by Markdown -export function extractLinks(text) { - text; // eslint-disable-line no-unused-expressions - Autolinker; // eslint-disable-line no-unused-expressions - - // skip this operation because autolinker is having issues - return []; - - /*const links = []; +export function extractFirstLink(text) { + const pattern = /(^|[\s\n]|<br\/?>)((?:https?|ftp):\/\/[\-A-Z0-9+\u0026\u2019@#\/%?=()~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~()_|])/i; let inText = text; // strip out code blocks @@ -330,32 +323,12 @@ export function extractLinks(text) { // strip out inline markdown images inText = inText.replace(/!\[[^\]]*\]\([^\)]*\)/g, ''); - function replaceFn(autolinker, match) { - let link = ''; - const matchText = match.getMatchedText(); - - if (matchText.trim().indexOf('http') === 0) { - link = matchText; - } else { - link = 'http://' + matchText; - } - - links.push(link); + const match = pattern.exec(inText); + if (match) { + return match[0].trim(); } - Autolinker.link( - inText, - { - replaceFn, - urls: {schemeMatches: true, wwwMatches: true, tldMatches: false}, - emails: false, - twitter: false, - phone: false, - hashtag: false - } - ); - - return links;*/ + return ''; } export function escapeRegExp(string) { @@ -651,7 +624,7 @@ export function applyTheme(theme) { } if (theme.sidebarHeaderBg) { - changeCss('.sidebar--left .team__header, .sidebar--menu .team__header, .post-list__timestamp', 'background:' + theme.sidebarHeaderBg, 1); + changeCss('.sidebar--left .team__header, .sidebar--menu .team__header, .post-list__timestamp > div', 'background:' + theme.sidebarHeaderBg, 1); changeCss('.modal .modal-header', 'background:' + theme.sidebarHeaderBg, 1); changeCss('#navbar .navbar-default', 'background:' + theme.sidebarHeaderBg, 1); changeCss('@media(max-width: 768px){.search-bar__container', 'background:' + theme.sidebarHeaderBg, 1); @@ -659,7 +632,7 @@ export function applyTheme(theme) { } if (theme.sidebarHeaderTextColor) { - changeCss('.sidebar--left .team__header .header__info, .sidebar--menu .team__header .header__info, .post-list__timestamp', 'color:' + theme.sidebarHeaderTextColor, 1); + changeCss('.sidebar--left .team__header .header__info, .sidebar--menu .team__header .header__info, .post-list__timestamp > div', 'color:' + theme.sidebarHeaderTextColor, 1); changeCss('.sidebar--left .team__header .navbar-right .dropdown__icon, .sidebar--menu .team__header .navbar-right .dropdown__icon', 'fill:' + theme.sidebarHeaderTextColor, 1); changeCss('.sidebar--left .team__header .user__name, .sidebar--menu .team__header .user__name', 'color:' + changeOpacity(theme.sidebarHeaderTextColor, 0.8), 1); changeCss('.sidebar--left .team__header:hover .user__name, .sidebar--menu .team__header:hover .user__name', 'color:' + theme.sidebarHeaderTextColor, 1); @@ -756,6 +729,7 @@ export function applyTheme(theme) { changeCss('.search-help-popover .search-autocomplete__item.selected', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1); changeCss('::-webkit-scrollbar-thumb', 'background:' + changeOpacity(theme.centerChannelColor, 0.4), 1); changeCss('body', 'scrollbar-arrow-color:' + theme.centerChannelColor, 4); + changeCss('.modal .about-modal .about-modal__logo svg, .post .post__img svg', 'fill:' + theme.centerChannelColor, 1); } if (theme.newMessageSeparator) { @@ -795,6 +769,10 @@ export function applyTheme(theme) { updateCodeTheme(theme.codeTheme); } +export function resetTheme() { + applyTheme(Constants.THEMES.default); +} + export function applyFont(fontName) { const body = $('body'); @@ -957,24 +935,6 @@ export function isValidUsername(name) { return error; } -export function updateAddressBar(channelName) { - const teamURL = TeamStore.getCurrentTeamUrl(); - history.replaceState('data', '', teamURL + '/channels/' + channelName); -} - -export function switchChannel(channel) { - GlobalActions.emitChannelClickEvent(channel); - - updateAddressBar(channel.name); - - $('.inner-wrap').removeClass('move--right'); - $('.sidebar--left').removeClass('move--right'); - - client.trackPage(); - - return false; -} - export function isMobile() { return screen.width <= 768; } @@ -1253,7 +1213,7 @@ export function importSlack(file, success, error) { } export function getTeamURLFromAddressBar() { - return window.location.href.split('/channels')[0]; + return window.location.origin + '/' + window.location.pathname.split('/')[1]; } export function getShortenedTeamURL() { @@ -1273,12 +1233,15 @@ export function windowHeight() { } export function openDirectChannelToUser(user, successCb, errorCb) { + AsyncClient.savePreference( + Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, + user.id, + 'true' + ); + const channelName = this.getDirectChannelName(UserStore.getCurrentId(), user.id); let channel = ChannelStore.getByName(channelName); - const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, user.id, 'true'); - AsyncClient.savePreferences([preference]); - if (channel) { if ($.isFunction(successCb)) { successCb(channel, true); diff --git a/webapp/webpack.config.js b/webapp/webpack.config.js index ee5c7e70b..4e2d6b70d 100644 --- a/webapp/webpack.config.js +++ b/webapp/webpack.config.js @@ -8,8 +8,12 @@ const htmlExtract = new ExtractTextPlugin('html', 'root.html'); const NPM_TARGET = process.env.npm_lifecycle_event; //eslint-disable-line no-process-env var DEV = false; -if (NPM_TARGET === 'run') { +var FULLMAP = false; +if (NPM_TARGET === 'run' || NPM_TARGET === 'run-fullmap') { DEV = true; + if (NPM_TARGET === 'run-fullmap') { + FULLMAP = true; + } } var config = { @@ -52,7 +56,7 @@ var config = { loaders: ['style', 'css'] }, { - test: /\.(png|eot|tiff|svg|woff2|woff|ttf|gif|mp3)$/, + test: /\.(png|eot|tiff|svg|woff2|woff|ttf|gif|mp3|jpg)$/, loader: 'file', query: { name: 'files/[hash].[ext]' @@ -73,7 +77,9 @@ var config = { }), htmlExtract, new CopyWebpackPlugin([ - {from: 'images/emoji', to: 'emoji'} + {from: 'images/emoji', to: 'emoji'}, + {from: 'images/logo-email.png', to: 'images'}, + {from: 'images/circles.png', to: 'images'} ]), new webpack.LoaderOptionsPlugin({ minimize: !DEV, @@ -94,7 +100,11 @@ var config = { // Development mode configuration if (DEV) { - config.devtool = 'eval-cheap-module-source-map'; + if (FULLMAP) { + config.devtool = 'source-map'; + } else { + config.devtool = 'eval-cheap-module-source-map'; + } } // Production mode configuration |