diff options
Diffstat (limited to 'web')
28 files changed, 812 insertions, 194 deletions
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index d9e67836d..abad60154 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -23,6 +23,7 @@ export default class CreatePost extends React.Component { this.lastTime = 0; + this.getCurrentDraft = this.getCurrentDraft.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.postMsgKeyPress = this.postMsgKeyPress.bind(this); this.handleUserInput = this.handleUserInput.bind(this); @@ -36,23 +37,15 @@ export default class CreatePost extends React.Component { PostStore.clearDraftUploads(); - const draft = PostStore.getCurrentDraft(); - let previews = []; - let messageText = ''; - let uploadsInProgress = []; - if (draft && draft.previews && draft.message) { - previews = draft.previews; - messageText = draft.message; - uploadsInProgress = draft.uploadsInProgress; - } + const draft = this.getCurrentDraft(); this.state = { channelId: ChannelStore.getCurrentId(), - messageText: messageText, - uploadsInProgress: uploadsInProgress, - previews: previews, + messageText: draft.messageText, + uploadsInProgress: draft.uploadsInProgress, + previews: draft.previews, submitting: false, - initialText: messageText + initialText: draft.messageText }; } componentDidUpdate(prevProps, prevState) { @@ -60,6 +53,24 @@ export default class CreatePost extends React.Component { this.resizePostHolder(); } } + getCurrentDraft() { + const draft = PostStore.getCurrentDraft(); + const safeDraft = {previews: [], messageText: '', uploadsInProgress: []}; + + if (draft) { + if (draft.message) { + safeDraft.messageText = draft.message; + } + if (draft.previews) { + safeDraft.previews = draft.previews; + } + if (draft.uploadsInProgress) { + safeDraft.uploadsInProgress = draft.uploadsInProgress; + } + } + + return safeDraft; + } handleSubmit(e) { e.preventDefault(); @@ -253,18 +264,9 @@ export default class CreatePost extends React.Component { onChange() { const channelId = ChannelStore.getCurrentId(); if (this.state.channelId !== channelId) { - let draft = PostStore.getCurrentDraft(); - - let previews = []; - let messageText = ''; - let uploadsInProgress = []; - if (draft && draft.previews && draft.message) { - previews = draft.previews; - messageText = draft.message; - uploadsInProgress = draft.uploadsInProgress; - } + const draft = this.getCurrentDraft(); - this.setState({channelId: channelId, messageText: messageText, initialText: messageText, submitting: false, serverError: null, postError: null, previews: previews, uploadsInProgress: uploadsInProgress}); + this.setState({channelId: channelId, messageText: draft.messageText, initialText: draft.messageText, submitting: false, serverError: null, postError: null, previews: draft.previews, uploadsInProgress: draft.uploadsInProgress}); } } getFileCount(channelId) { diff --git a/web/react/components/email_verify.jsx b/web/react/components/email_verify.jsx index 92123956f..8d3f15525 100644 --- a/web/react/components/email_verify.jsx +++ b/web/react/components/email_verify.jsx @@ -10,12 +10,14 @@ export default class EmailVerify extends React.Component { this.state = {}; } handleResend() { - window.location.href = window.location.href + '&resend=true'; + const newAddress = window.location.href.replace('&resend_success=true', ''); + window.location.href = newAddress + '&resend=true'; } render() { var title = ''; var body = ''; var resend = ''; + var resendConfirm = ''; if (this.props.isVerified === 'true') { title = global.window.config.SiteName + ' Email Verified'; body = <p>Your email has been verified! Click <a href={this.props.teamURL + '?email=' + this.props.userEmail}>here</a> to log in.</p>; @@ -30,6 +32,9 @@ export default class EmailVerify extends React.Component { Resend Email </button> ); + if (this.props.resendSuccess) { + resendConfirm = <div><br /><p className='alert alert-success'><i className='fa fa-check'></i>{' Verification email sent.'}</p></div>; + } } return ( @@ -41,6 +46,7 @@ export default class EmailVerify extends React.Component { <div className='panel-body'> {body} {resend} + {resendConfirm} </div> </div> </div> @@ -51,10 +57,12 @@ export default class EmailVerify extends React.Component { EmailVerify.defaultProps = { isVerified: 'false', teamURL: '', - userEmail: '' + userEmail: '', + resendSuccess: 'false' }; EmailVerify.propTypes = { isVerified: React.PropTypes.string, teamURL: React.PropTypes.string, - userEmail: React.PropTypes.string + userEmail: React.PropTypes.string, + resendSuccess: React.PropTypes.string }; diff --git a/web/react/components/new_channel_modal.jsx b/web/react/components/new_channel_modal.jsx index 99d5b49e9..c8ef59b4a 100644 --- a/web/react/components/new_channel_modal.jsx +++ b/web/react/components/new_channel_modal.jsx @@ -151,7 +151,7 @@ export default class NewChannelModal extends React.Component { tabIndex='2' /> <p className='input__help'> - {'This is the purpose of your channel and helps others decide whether to join.'} + {'Description helps others decide whether to join this channel.'} </p> {serverError} </div> diff --git a/web/react/components/password_reset_send_link.jsx b/web/react/components/password_reset_send_link.jsx index 1e6cc3607..37d4a58cb 100644 --- a/web/react/components/password_reset_send_link.jsx +++ b/web/react/components/password_reset_send_link.jsx @@ -1,6 +1,7 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. +const Utils = require('../utils/utils.jsx'); var client = require('../utils/client.jsx'); export default class PasswordResetSendLink extends React.Component { @@ -15,8 +16,8 @@ export default class PasswordResetSendLink extends React.Component { e.preventDefault(); var state = {}; - var email = React.findDOMNode(this.refs.email).value.trim(); - if (!email) { + var email = React.findDOMNode(this.refs.email).value.trim().toLowerCase(); + if (!email || !Utils.isEmail(email)) { state.error = 'Please enter a valid email address.'; this.setState(state); return; @@ -67,7 +68,7 @@ export default class PasswordResetSendLink extends React.Component { <p>{'To reset your password, enter the email address you used to sign up for ' + this.props.teamDisplayName + '.'}</p> <div className={formClass}> <input - type='text' + type='email' className='form-control' name='email' ref='email' diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index e0682e997..dbbcdc409 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -35,7 +35,6 @@ export default class PostBody extends React.Component { parseEmojis() { twemoji.parse(React.findDOMNode(this), {size: Constants.EMOJI_SIZE}); - global.window.emojify.run(React.findDOMNode(this.refs.message_span)); } componentDidMount() { diff --git a/web/react/components/post_list_container.jsx b/web/react/components/post_list_container.jsx index 0815ac883..e59d85d41 100644 --- a/web/react/components/post_list_container.jsx +++ b/web/react/components/post_list_container.jsx @@ -49,6 +49,7 @@ export default class PostListContainer extends React.Component { for (let i = 0; i <= this.state.postLists.length - 1; i++) { postListCtls.push( <PostList + key={'postlistkey' + i} channelId={postLists[i]} isActive={postLists[i] === channelId} /> diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx index fe31ac381..4d1892a69 100644 --- a/web/react/components/rhs_comment.jsx +++ b/web/react/components/rhs_comment.jsx @@ -56,7 +56,6 @@ export default class RhsComment extends React.Component { } parseEmojis() { twemoji.parse(React.findDOMNode(this), {size: Constants.EMOJI_SIZE}); - global.window.emojify.run(React.findDOMNode(this.refs.message_holder)); } componentDidMount() { this.parseEmojis(); @@ -114,14 +113,7 @@ export default class RhsComment extends React.Component { var ownerOptions; if (isOwner && post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING) { ownerOptions = ( - <div - className='dropdown' - onClick={ - function scroll() { - $('.post-list-holder-by-time').scrollTop($('.post-list-holder-by-time').scrollTop() + 50); - } - } - > + <div className='dropdown'> <a href='#' className='dropdown-toggle theme' diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx index 2ea697c5b..86620a499 100644 --- a/web/react/components/rhs_root_post.jsx +++ b/web/react/components/rhs_root_post.jsx @@ -20,7 +20,6 @@ export default class RhsRootPost extends React.Component { } parseEmojis() { twemoji.parse(React.findDOMNode(this), {size: Constants.EMOJI_SIZE}); - global.window.emojify.run(React.findDOMNode(this.refs.message_holder)); } componentDidMount() { this.parseEmojis(); diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index 19c3b2d22..e77bde861 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -1,7 +1,7 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var utils = require('../utils/utils.jsx'); +var Utils = require('../utils/utils.jsx'); var client = require('../utils/client.jsx'); var UserStore = require('../stores/user_store.jsx'); var BrowserStore = require('../stores/browser_store.jsx'); @@ -31,13 +31,26 @@ export default class SignupUserComplete extends React.Component { handleSubmit(e) { e.preventDefault(); + const providedEmail = React.findDOMNode(this.refs.email).value.trim(); + if (!providedEmail) { + this.setState({nameError: '', emailError: 'This field is required', passwordError: ''}); + return; + } + + if (!Utils.isEmail(providedEmail)) { + this.setState({nameError: '', emailError: 'Please enter a valid email address', passwordError: ''}); + return; + } + + this.state.user.email = providedEmail; + this.state.user.username = React.findDOMNode(this.refs.name).value.trim().toLowerCase(); if (!this.state.user.username) { this.setState({nameError: 'This field is required', emailError: '', passwordError: '', serverError: ''}); return; } - var usernameError = utils.isValidUsername(this.state.user.username); + var usernameError = Utils.isValidUsername(this.state.user.username); if (usernameError === 'Cannot use a reserved word as a username.') { this.setState({nameError: 'This username is reserved, please choose a new one.', emailError: '', passwordError: '', serverError: ''}); return; @@ -51,12 +64,6 @@ export default class SignupUserComplete extends React.Component { return; } - this.state.user.email = React.findDOMNode(this.refs.email).value.trim(); - if (!this.state.user.email) { - this.setState({nameError: '', emailError: 'This field is required', passwordError: ''}); - return; - } - this.state.user.password = React.findDOMNode(this.refs.password).value.trim(); if (!this.state.user.password || this.state.user.password .length < 5) { this.setState({nameError: '', emailError: '', passwordError: 'Please enter at least 5 characters', serverError: ''}); diff --git a/web/react/components/user_settings/manage_incoming_hooks.jsx b/web/react/components/user_settings/manage_incoming_hooks.jsx new file mode 100644 index 000000000..df089a403 --- /dev/null +++ b/web/react/components/user_settings/manage_incoming_hooks.jsx @@ -0,0 +1,177 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var Client = require('../../utils/client.jsx'); +var Utils = require('../../utils/utils.jsx'); +var Constants = require('../../utils/constants.jsx'); +var ChannelStore = require('../../stores/channel_store.jsx'); +var LoadingScreen = require('../loading_screen.jsx'); + +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() { + let hook = {}; //eslint-disable-line prefer-const + 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) { + let data = {}; //eslint-disable-line prefer-const + data.id = id; + + Client.deleteIncomingHook( + data, + () => { + let hooks = this.state.hooks; //eslint-disable-line prefer-const + 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) => { + let state = this.state; //eslint-disable-line prefer-const + + 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(); + let options = []; //eslint-disable-line prefer-const + channels.forEach((channel) => { + options.push(<option value={channel.id}>{channel.name}</option>); + }); + + let disableButton = ''; + if (this.state.channelId === '') { + disableButton = ' disable'; + } + + let hooks = []; //eslint-disable-line prefer-const + this.state.hooks.forEach((hook) => { + const c = ChannelStore.get(hook.channel_id); + hooks.push( + <div> + <div className='divider-light'></div> + <span> + <strong>{'URL: '}</strong>{Utils.getWindowLocationOrigin() + '/hooks/' + hook.id} + </span> + <br/> + <span> + <strong>{'Channel: '}</strong>{c.name} + </span> + <br/> + <a + className={'btn btn-sm btn-primary'} + href='#' + onClick={this.removeHook.bind(this, hook.id)} + > + {'Remove'} + </a> + </div> + ); + }); + + let displayHooks; + if (!this.state.getHooksComplete) { + displayHooks = <LoadingScreen/>; + } else if (hooks.length > 0) { + displayHooks = hooks; + } else { + displayHooks = <label>{'None'}</label>; + } + + const existingHooks = ( + <div> + <label className='control-label'>{'Existing incoming webhooks'}</label> + <br/> + {displayHooks} + </div> + ); + + return ( + <div + key='addIncomingHook' + className='form-group' + > + <label className='control-label'>{'Add a new incoming webhook'}</label> + <br/> + <div> + <select + ref='channelName' + value={this.state.channelId} + onChange={this.updateChannelId} + > + {options} + </select> + <br/> + {serverError} + <a + className={'btn btn-sm btn-primary' + disableButton} + href='#' + onClick={this.addNewHook} + > + {'Add'} + </a> + </div> + {existingHooks} + </div> + ); + } +} diff --git a/web/react/components/user_settings.jsx b/web/react/components/user_settings/user_settings.jsx index 48b499068..0eab333c4 100644 --- a/web/react/components/user_settings.jsx +++ b/web/react/components/user_settings/user_settings.jsx @@ -1,13 +1,14 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var UserStore = require('../stores/user_store.jsx'); -var utils = require('../utils/utils.jsx'); +var UserStore = require('../../stores/user_store.jsx'); +var utils = require('../../utils/utils.jsx'); var NotificationsTab = require('./user_settings_notifications.jsx'); var SecurityTab = require('./user_settings_security.jsx'); var GeneralTab = require('./user_settings_general.jsx'); var AppearanceTab = require('./user_settings_appearance.jsx'); var DeveloperTab = require('./user_settings_developer.jsx'); +var IntegrationsTab = require('./user_settings_integrations.jsx'); export default class UserSettings extends React.Component { constructor(props) { @@ -86,6 +87,17 @@ export default class UserSettings extends React.Component { /> </div> ); + } else if (this.props.activeTab === 'integrations') { + return ( + <div> + <IntegrationsTab + user={this.state.user} + activeSection={this.props.activeSection} + updateSection={this.props.updateSection} + updateTab={this.props.updateTab} + /> + </div> + ); } return <div/>; diff --git a/web/react/components/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx index 3df013d03..aec3b319d 100644 --- a/web/react/components/user_settings_appearance.jsx +++ b/web/react/components/user_settings/user_settings_appearance.jsx @@ -1,11 +1,11 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var UserStore = require('../stores/user_store.jsx'); -var SettingItemMin = require('./setting_item_min.jsx'); -var SettingItemMax = require('./setting_item_max.jsx'); -var Client = require('../utils/client.jsx'); -var Utils = require('../utils/utils.jsx'); +var UserStore = require('../../stores/user_store.jsx'); +var SettingItemMin = require('../setting_item_min.jsx'); +var SettingItemMax = require('../setting_item_max.jsx'); +var Client = require('../../utils/client.jsx'); +var Utils = require('../../utils/utils.jsx'); var ThemeColors = ['#2389d7', '#008a17', '#dc4fad', '#ac193d', '#0072c6', '#d24726', '#ff8f32', '#82ba00', '#03b3b2', '#008299', '#4617b4', '#8c0095', '#004b8b', '#004b8b', '#570000', '#380000', '#585858', '#000000']; diff --git a/web/react/components/user_settings_developer.jsx b/web/react/components/user_settings/user_settings_developer.jsx index 1b04149dc..1694aaa79 100644 --- a/web/react/components/user_settings_developer.jsx +++ b/web/react/components/user_settings/user_settings_developer.jsx @@ -1,8 +1,8 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var SettingItemMin = require('./setting_item_min.jsx'); -var SettingItemMax = require('./setting_item_max.jsx'); +var SettingItemMin = require('../setting_item_min.jsx'); +var SettingItemMax = require('../setting_item_max.jsx'); export default class DeveloperTab extends React.Component { constructor(props) { diff --git a/web/react/components/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx index 66cde6ca2..5d9d9bfde 100644 --- a/web/react/components/user_settings_general.jsx +++ b/web/react/components/user_settings/user_settings_general.jsx @@ -1,13 +1,13 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var UserStore = require('../stores/user_store.jsx'); -var SettingItemMin = require('./setting_item_min.jsx'); -var SettingItemMax = require('./setting_item_max.jsx'); -var SettingPicture = require('./setting_picture.jsx'); -var client = require('../utils/client.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); -var utils = require('../utils/utils.jsx'); +var UserStore = require('../../stores/user_store.jsx'); +var SettingItemMin = require('../setting_item_min.jsx'); +var SettingItemMax = require('../setting_item_max.jsx'); +var SettingPicture = require('../setting_picture.jsx'); +var client = require('../../utils/client.jsx'); +var AsyncClient = require('../../utils/async_client.jsx'); +var utils = require('../../utils/utils.jsx'); var assign = require('object-assign'); export default class UserSettingsGeneralTab extends React.Component { diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx new file mode 100644 index 000000000..cb45c5178 --- /dev/null +++ b/web/react/components/user_settings/user_settings_integrations.jsx @@ -0,0 +1,95 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var SettingItemMin = require('../setting_item_min.jsx'); +var SettingItemMax = require('../setting_item_max.jsx'); +var ManageIncomingHooks = require('./manage_incoming_hooks.jsx'); + +export default class UserSettingsIntegrationsTab extends React.Component { + constructor(props) { + super(props); + + this.updateSection = this.updateSection.bind(this); + this.handleClose = this.handleClose.bind(this); + + this.state = {}; + } + updateSection(section) { + this.props.updateSection(section); + } + handleClose() { + this.updateSection(''); + } + componentDidMount() { + $('#user_settings').on('hidden.bs.modal', this.handleClose); + } + componentWillUnmount() { + $('#user_settings').off('hidden.bs.modal', this.handleClose); + } + render() { + let incomingHooksSection; + var inputs = []; + + if (this.props.activeSection === 'incoming-hooks') { + inputs.push( + <ManageIncomingHooks /> + ); + + incomingHooksSection = ( + <SettingItemMax + title='Incoming Webhooks' + inputs={inputs} + updateSection={function clearSection(e) { + this.updateSection(''); + e.preventDefault(); + }.bind(this)} + /> + ); + } else { + incomingHooksSection = ( + <SettingItemMin + title='Incoming Webhooks' + describe='Manage your incoming webhooks' + updateSection={function updateNameSection() { + this.updateSection('incoming-hooks'); + }.bind(this)} + /> + ); + } + + return ( + <div> + <div className='modal-header'> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>{'×'}</span> + </button> + <h4 + className='modal-title' + ref='title' + > + <i className='modal-back'></i> + {'Integration Settings'} + </h4> + </div> + <div className='user-settings'> + <h3 className='tab-header'>{'Integration Settings'}</h3> + <div className='divider-dark first'/> + {incomingHooksSection} + <div className='divider-dark'/> + </div> + </div> + ); + } +} + +UserSettingsIntegrationsTab.propTypes = { + user: React.PropTypes.object, + updateSection: React.PropTypes.func, + updateTab: React.PropTypes.func, + activeSection: React.PropTypes.string +}; diff --git a/web/react/components/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx index 67a4d0041..1b22e6045 100644 --- a/web/react/components/user_settings_modal.jsx +++ b/web/react/components/user_settings/user_settings_modal.jsx @@ -1,7 +1,7 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var SettingsSidebar = require('./settings_sidebar.jsx'); +var SettingsSidebar = require('../settings_sidebar.jsx'); var UserSettings = require('./user_settings.jsx'); export default class UserSettingsModal extends React.Component { @@ -38,6 +38,9 @@ export default class UserSettingsModal extends React.Component { if (global.window.config.EnableOAuthServiceProvider === 'true') { tabs.push({name: 'developer', uiName: 'Developer', icon: 'glyphicon glyphicon-th'}); } + if (global.window.config.AllowIncomingWebhooks === 'true') { + tabs.push({name: 'integrations', uiName: 'Integrations', icon: 'glyphicon glyphicon-transfer'}); + } return ( <div diff --git a/web/react/components/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx index dadbb669b..fde4970ce 100644 --- a/web/react/components/user_settings_notifications.jsx +++ b/web/react/components/user_settings/user_settings_notifications.jsx @@ -1,12 +1,12 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var UserStore = require('../stores/user_store.jsx'); -var SettingItemMin = require('./setting_item_min.jsx'); -var SettingItemMax = require('./setting_item_max.jsx'); -var client = require('../utils/client.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); -var utils = require('../utils/utils.jsx'); +var UserStore = require('../../stores/user_store.jsx'); +var SettingItemMin = require('../setting_item_min.jsx'); +var SettingItemMax = require('../setting_item_max.jsx'); +var client = require('../../utils/client.jsx'); +var AsyncClient = require('../../utils/async_client.jsx'); +var utils = require('../../utils/utils.jsx'); var assign = require('object-assign'); function getNotificationsStateFromStores() { diff --git a/web/react/components/user_settings_security.jsx b/web/react/components/user_settings/user_settings_security.jsx index c10d790ae..b59c08af0 100644 --- a/web/react/components/user_settings_security.jsx +++ b/web/react/components/user_settings/user_settings_security.jsx @@ -1,11 +1,11 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var SettingItemMin = require('./setting_item_min.jsx'); -var SettingItemMax = require('./setting_item_max.jsx'); -var Client = require('../utils/client.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); -var Constants = require('../utils/constants.jsx'); +var SettingItemMin = require('../setting_item_min.jsx'); +var SettingItemMax = require('../setting_item_max.jsx'); +var Client = require('../../utils/client.jsx'); +var AsyncClient = require('../../utils/async_client.jsx'); +var Constants = require('../../utils/constants.jsx'); export default class SecurityTab extends React.Component { constructor(props) { diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx index 43493de45..d24fe0b98 100644 --- a/web/react/pages/channel.jsx +++ b/web/react/pages/channel.jsx @@ -19,7 +19,7 @@ var DeletePostModal = require('../components/delete_post_modal.jsx'); var MoreChannelsModal = require('../components/more_channels.jsx'); var PostDeletedModal = require('../components/post_deleted_modal.jsx'); var ChannelNotificationsModal = require('../components/channel_notifications.jsx'); -var UserSettingsModal = require('../components/user_settings_modal.jsx'); +var UserSettingsModal = require('../components/user_settings/user_settings_modal.jsx'); var TeamSettingsModal = require('../components/team_settings_modal.jsx'); var ChannelMembersModal = require('../components/channel_members.jsx'); var ChannelInviteModal = require('../components/channel_invite_modal.jsx'); diff --git a/web/react/pages/verify.jsx b/web/react/pages/verify.jsx index e48471bbd..16a9846e5 100644 --- a/web/react/pages/verify.jsx +++ b/web/react/pages/verify.jsx @@ -9,6 +9,7 @@ global.window.setupVerifyPage = function setupVerifyPage(props) { isVerified={props.IsVerified} teamURL={props.TeamURL} userEmail={props.UserEmail} + resendSuccess={props.ResendSuccess} />, document.getElementById('verify') ); diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index c9eb09c00..531e4fdae 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -59,7 +59,7 @@ export function createTeamFromSignup(teamSignup, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify(teamSignup), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('createTeamFromSignup', xhr, status, err); error(e); @@ -74,7 +74,7 @@ export function createTeamWithSSO(team, service, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify(team), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('createTeamWithSSO', xhr, status, err); error(e); @@ -89,7 +89,7 @@ export function createUser(user, data, emailHash, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify(user), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('createUser', xhr, status, err); error(e); @@ -106,7 +106,7 @@ export function updateUser(user, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify(user), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('updateUser', xhr, status, err); error(e); @@ -123,7 +123,7 @@ export function updatePassword(data, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify(data), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('newPassword', xhr, status, err); error(e); @@ -140,7 +140,7 @@ export function updateUserNotifyProps(data, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify(data), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('updateUserNotifyProps', xhr, status, err); error(e); @@ -155,7 +155,7 @@ export function updateRoles(data, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify(data), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('updateRoles', xhr, status, err); error(e); @@ -176,7 +176,7 @@ export function updateActive(userId, active, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify(data), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('updateActive', xhr, status, err); error(e); @@ -193,7 +193,7 @@ export function sendPasswordReset(data, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify(data), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('sendPasswordReset', xhr, status, err); error(e); @@ -210,7 +210,7 @@ export function resetPassword(data, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify(data), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('resetPassword', xhr, status, err); error(e); @@ -254,7 +254,7 @@ export function revokeSession(altId, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify({id: altId}), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('revokeSession', xhr, status, err); error(e); @@ -269,7 +269,7 @@ export function getSessions(userId, success, error) { dataType: 'json', contentType: 'application/json', type: 'GET', - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('getSessions', xhr, status, err); error(e); @@ -283,7 +283,7 @@ export function getAudits(userId, success, error) { dataType: 'json', contentType: 'application/json', type: 'GET', - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('getAudits', xhr, status, err); error(e); @@ -367,7 +367,7 @@ export function inviteMembers(data, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify(data), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('inviteMembers', xhr, status, err); error(e); @@ -384,7 +384,7 @@ export function updateTeamDisplayName(data, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify(data), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('updateTeamDisplayName', xhr, status, err); error(e); @@ -401,7 +401,7 @@ export function signupTeam(email, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify({email: email}), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('singupTeam', xhr, status, err); error(e); @@ -418,7 +418,7 @@ export function createTeam(team, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify(team), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('createTeam', xhr, status, err); error(e); @@ -433,7 +433,7 @@ export function findTeamByName(teamName, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify({name: teamName}), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('findTeamByName', xhr, status, err); error(e); @@ -448,7 +448,7 @@ export function findTeamsSendEmail(email, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify({email: email}), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('findTeamsSendEmail', xhr, status, err); error(e); @@ -465,7 +465,7 @@ export function findTeams(email, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify({email: email}), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('findTeams', xhr, status, err); error(e); @@ -480,7 +480,7 @@ export function createChannel(channel, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify(channel), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('createChannel', xhr, status, err); error(e); @@ -497,7 +497,7 @@ export function createDirectChannel(channel, userId, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify({user_id: userId}), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('createDirectChannel', xhr, status, err); error(e); @@ -514,7 +514,7 @@ export function updateChannel(channel, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify(channel), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('updateChannel', xhr, status, err); error(e); @@ -531,7 +531,7 @@ export function updateChannelDesc(data, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify(data), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('updateChannelDesc', xhr, status, err); error(e); @@ -548,7 +548,7 @@ export function updateNotifyLevel(data, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify(data), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('updateNotifyLevel', xhr, status, err); error(e); @@ -562,7 +562,7 @@ export function joinChannel(id, success, error) { dataType: 'json', contentType: 'application/json', type: 'POST', - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('joinChannel', xhr, status, err); error(e); @@ -578,7 +578,7 @@ export function leaveChannel(id, success, error) { dataType: 'json', contentType: 'application/json', type: 'POST', - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('leaveChannel', xhr, status, err); error(e); @@ -594,7 +594,7 @@ export function deleteChannel(id, success, error) { dataType: 'json', contentType: 'application/json', type: 'POST', - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('deleteChannel', xhr, status, err); error(e); @@ -610,7 +610,7 @@ export function updateLastViewedAt(channelId, success, error) { dataType: 'json', contentType: 'application/json', type: 'POST', - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('updateLastViewedAt', xhr, status, err); error(e); @@ -624,7 +624,7 @@ export function getChannels(success, error) { url: '/api/v1/channels/', dataType: 'json', type: 'GET', - success: success, + success, ifModified: true, error: function onError(xhr, status, err) { var e = handleError('getChannels', xhr, status, err); @@ -639,7 +639,7 @@ export function getChannel(id, success, error) { url: '/api/v1/channels/' + id + '/', dataType: 'json', type: 'GET', - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('getChannel', xhr, status, err); error(e); @@ -654,7 +654,7 @@ export function getMoreChannels(success, error) { url: '/api/v1/channels/more', dataType: 'json', type: 'GET', - success: success, + success, ifModified: true, error: function onError(xhr, status, err) { var e = handleError('getMoreChannels', xhr, status, err); @@ -669,7 +669,7 @@ export function getChannelCounts(success, error) { url: '/api/v1/channels/counts', dataType: 'json', type: 'GET', - success: success, + success, ifModified: true, error: function onError(xhr, status, err) { var e = handleError('getChannelCounts', xhr, status, err); @@ -683,7 +683,7 @@ export function getChannelExtraInfo(id, success, error) { url: '/api/v1/channels/' + id + '/extra_info', dataType: 'json', type: 'GET', - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('getChannelExtraInfo', xhr, status, err); error(e); @@ -698,7 +698,7 @@ export function executeCommand(channelId, command, suggest, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify({channelId: channelId, command: command, suggest: '' + suggest}), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('executeCommand', xhr, status, err); error(e); @@ -713,7 +713,7 @@ export function getPostsPage(channelId, offset, limit, success, error, complete) dataType: 'json', type: 'GET', ifModified: true, - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('getPosts', xhr, status, err); error(e); @@ -728,7 +728,7 @@ export function getPosts(channelId, since, success, error, complete) { dataType: 'json', type: 'GET', ifModified: true, - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('getPosts', xhr, status, err); error(e); @@ -744,7 +744,7 @@ export function getPost(channelId, postId, success, error) { dataType: 'json', type: 'GET', ifModified: false, - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('getPost', xhr, status, err); error(e); @@ -758,7 +758,7 @@ export function search(terms, success, error) { dataType: 'json', type: 'GET', data: {terms: terms}, - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('search', xhr, status, err); error(e); @@ -774,7 +774,7 @@ export function deletePost(channelId, id, success, error) { dataType: 'json', contentType: 'application/json', type: 'POST', - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('deletePost', xhr, status, err); error(e); @@ -791,7 +791,7 @@ export function createPost(post, channel, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify(post), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('createPost', xhr, status, err); error(e); @@ -817,7 +817,7 @@ export function updatePost(post, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify(post), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('updatePost', xhr, status, err); error(e); @@ -834,7 +834,7 @@ export function addChannelMember(id, data, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify(data), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('addChannelMember', xhr, status, err); error(e); @@ -851,7 +851,7 @@ export function removeChannelMember(id, data, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify(data), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('removeChannelMember', xhr, status, err); error(e); @@ -868,7 +868,7 @@ export function getProfiles(success, error) { dataType: 'json', contentType: 'application/json', type: 'GET', - success: success, + success, ifModified: true, error: function onError(xhr, status, err) { var e = handleError('getProfiles', xhr, status, err); @@ -885,7 +885,7 @@ export function uploadFile(formData, success, error) { cache: false, contentType: false, processData: false, - success: success, + success, error: function onError(xhr, status, err) { if (err !== 'abort') { var e = handleError('uploadFile', xhr, status, err); @@ -905,7 +905,7 @@ export function getFileInfo(filename, success, error) { dataType: 'json', contentType: 'application/json', type: 'GET', - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('getFileInfo', xhr, status, err); error(e); @@ -919,7 +919,7 @@ export function getPublicLink(data, success, error) { dataType: 'json', type: 'POST', data: JSON.stringify(data), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('getPublicLink', xhr, status, err); error(e); @@ -935,7 +935,7 @@ export function uploadProfileImage(imageData, success, error) { cache: false, contentType: false, processData: false, - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('uploadProfileImage', xhr, status, err); error(e); @@ -951,7 +951,7 @@ export function importSlack(fileData, success, error) { cache: false, contentType: false, processData: false, - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('importTeam', xhr, status, err); error(e); @@ -964,7 +964,7 @@ export function exportTeam(success, error) { url: '/api/v1/teams/export_team', type: 'GET', dataType: 'json', - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('exportTeam', xhr, status, err); error(e); @@ -978,7 +978,7 @@ export function getStatuses(success, error) { dataType: 'json', contentType: 'application/json', type: 'GET', - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('getStatuses', xhr, status, err); error(e); @@ -991,7 +991,7 @@ export function getMyTeam(success, error) { url: '/api/v1/teams/me', dataType: 'json', type: 'GET', - success: success, + success, ifModified: true, error: function onError(xhr, status, err) { var e = handleError('getMyTeam', xhr, status, err); @@ -1007,7 +1007,7 @@ export function updateValetFeature(data, success, error) { contentType: 'application/json', type: 'POST', data: JSON.stringify(data), - success: success, + success, error: function onError(xhr, status, err) { var e = handleError('updateValetFeature', xhr, status, err); error(e); @@ -1040,7 +1040,7 @@ export function allowOAuth2(responseType, clientId, redirectUri, state, scope, s dataType: 'json', contentType: 'application/json', type: 'GET', - success: success, + success, error: (xhr, status, err) => { const e = handleError('allowOAuth2', xhr, status, err); error(e); @@ -1049,3 +1049,46 @@ export function allowOAuth2(responseType, clientId, redirectUri, state, scope, s module.exports.track('api', 'api_users_allow_oauth2'); } + +export function addIncomingHook(hook, success, error) { + $.ajax({ + url: '/api/v1/hooks/incoming/create', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(hook), + success, + error: (xhr, status, err) => { + var e = handleError('addIncomingHook', xhr, status, err); + error(e); + } + }); +} + +export function deleteIncomingHook(data, success, error) { + $.ajax({ + url: '/api/v1/hooks/incoming/delete', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(data), + success, + error: (xhr, status, err) => { + var e = handleError('deleteIncomingHook', xhr, status, err); + error(e); + } + }); +} + +export function listIncomingHooks(success, error) { + $.ajax({ + url: '/api/v1/hooks/incoming/list', + dataType: 'json', + type: 'GET', + success, + error: (xhr, status, err) => { + var e = handleError('listIncomingHooks', xhr, status, err); + error(e); + } + }); +} diff --git a/web/react/utils/emoticons.jsx b/web/react/utils/emoticons.jsx new file mode 100644 index 000000000..7210201ff --- /dev/null +++ b/web/react/utils/emoticons.jsx @@ -0,0 +1,159 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +const emoticonPatterns = { + smile: /:-?\)/g, // :) + open_mouth: /:o/gi, // :o + scream: /:-o/gi, // :-o + smirk: /[:;]-?]/g, // :] + grinning: /[:;]-?d/gi, // :D + stuck_out_tongue_closed_eyes: /x-d/gi, // x-d + stuck_out_tongue_winking_eye: /[:;]-?p/gi, // ;p + rage: /:-?[\[@]/g, // :@ + frowning: /:-?\(/g, // :( + sob: /:['’]-?\(|:'\(/g, // :`( + kissing_heart: /:-?\*/g, // :* + wink: /;-?\)/g, // ;) + pensive: /:-?\//g, // :/ + confounded: /:-?s/gi, // :s + flushed: /:-?\|/g, // :| + relaxed: /:-?\$/g, // :$ + mask: /:-x/gi, // :-x + heart: /<3|<3/g, // <3 + broken_heart: /<\/3|</3/g, // </3 + thumbsup: /:\+1:/g, // :+1: + thumbsdown: /:\-1:/g // :-1: +}; + +function initializeEmoticonMap() { + const emoticonNames = + ('+1,-1,100,1234,8ball,a,ab,abc,abcd,accept,aerial_tramway,airplane,alarm_clock,alien,ambulance,anchor,angel,' + + 'anger,angry,anguished,ant,apple,aquarius,aries,arrow_backward,arrow_double_down,arrow_double_up,arrow_down,' + + 'arrow_down_small,arrow_forward,arrow_heading_down,arrow_heading_up,arrow_left,arrow_lower_left,' + + 'arrow_lower_right,arrow_right,arrow_right_hook,arrow_up,arrow_up_down,arrow_up_small,arrow_upper_left,' + + 'arrow_upper_right,arrows_clockwise,arrows_counterclockwise,art,articulated_lorry,astonished,atm,b,baby,' + + 'baby_bottle,baby_chick,baby_symbol,back,baggage_claim,balloon,ballot_box_with_check,bamboo,banana,bangbang,' + + 'bank,bar_chart,barber,baseball,basketball,bath,bathtub,battery,bear,bee,beer,beers,beetle,beginner,bell,bento,' + + 'bicyclist,bike,bikini,bird,birthday,black_circle,black_joker,black_medium_small_square,black_medium_square,' + + 'black_nib,black_small_square,black_square,black_square_button,blossom,blowfish,blue_book,blue_car,blue_heart,' + + 'blush,boar,boat,bomb,book,bookmark,bookmark_tabs,books,boom,boot,bouquet,bow,bowling,bowtie,boy,bread,' + + 'bride_with_veil,bridge_at_night,briefcase,broken_heart,bug,bulb,bullettrain_front,bullettrain_side,bus,busstop,' + + 'bust_in_silhouette,busts_in_silhouette,cactus,cake,calendar,calling,camel,camera,cancer,candy,capital_abcd,' + + 'capricorn,car,card_index,carousel_horse,cat,cat2,cd,chart,chart_with_downwards_trend,chart_with_upwards_trend,' + + 'checkered_flag,cherries,cherry_blossom,chestnut,chicken,children_crossing,chocolate_bar,christmas_tree,church,' + + 'cinema,circus_tent,city_sunrise,city_sunset,cl,clap,clapper,clipboard,clock1,clock10,clock1030,clock11,' + + 'clock1130,clock12,clock1230,clock130,clock2,clock230,clock3,clock330,clock4,clock430,clock5,clock530,clock6,' + + 'clock630,clock7,clock730,clock8,clock830,clock9,clock930,closed_book,closed_lock_with_key,closed_umbrella,cloud,' + + 'clubs,cn,cocktail,coffee,cold_sweat,collision,computer,confetti_ball,confounded,confused,congratulations,' + + 'construction,construction_worker,convenience_store,cookie,cool,cop,copyright,corn,couple,couple_with_heart,' + + 'couplekiss,cow,cow2,credit_card,crescent_moon,crocodile,crossed_flags,crown,cry,crying_cat_face,crystal_ball,' + + 'cupid,curly_loop,currency_exchange,curry,custard,customs,cyclone,dancer,dancers,dango,dart,dash,date,de,' + + 'deciduous_tree,department_store,diamond_shape_with_a_dot_inside,diamonds,disappointed,disappointed_relieved,' + + 'dizzy,dizzy_face,do_not_litter,dog,dog2,dollar,dolls,dolphin,donut,door,doughnut,dragon,dragon_face,dress,' + + 'dromedary_camel,droplet,dvd,e-mail,ear,ear_of_rice,earth_africa,earth_americas,earth_asia,egg,eggplant,eight,' + + 'eight_pointed_black_star,eight_spoked_asterisk,electric_plug,elephant,email,end,envelope,es,euro,' + + 'european_castle,european_post_office,evergreen_tree,exclamation,expressionless,eyeglasses,eyes,facepunch,' + + 'factory,fallen_leaf,family,fast_forward,fax,fearful,feelsgood,feet,ferris_wheel,file_folder,finnadie,fire,' + + 'fire_engine,fireworks,first_quarter_moon,first_quarter_moon_with_face,fish,fish_cake,fishing_pole_and_fish,fist,' + + 'five,flags,flashlight,floppy_disk,flower_playing_cards,flushed,foggy,football,fork_and_knife,fountain,four,' + + 'four_leaf_clover,fr,free,fried_shrimp,fries,frog,frowning,fu,fuelpump,full_moon,full_moon_with_face,game_die,gb,' + + 'gem,gemini,ghost,gift,gift_heart,girl,globe_with_meridians,goat,goberserk,godmode,golf,grapes,green_apple,' + + 'green_book,green_heart,grey_exclamation,grey_question,grimacing,grin,grinning,guardsman,guitar,gun,haircut,' + + 'hamburger,hammer,hamster,hand,handbag,hankey,hash,hatched_chick,hatching_chick,headphones,hear_no_evil,heart,' + + 'heart_decoration,heart_eyes,heart_eyes_cat,heartbeat,heartpulse,hearts,heavy_check_mark,heavy_division_sign,' + + 'heavy_dollar_sign,heavy_exclamation_mark,heavy_minus_sign,heavy_multiplication_x,heavy_plus_sign,helicopter,' + + 'herb,hibiscus,high_brightness,high_heel,hocho,honey_pot,honeybee,horse,horse_racing,hospital,hotel,hotsprings,' + + 'hourglass,hourglass_flowing_sand,house,house_with_garden,hurtrealbad,hushed,ice_cream,icecream,id,' + + 'ideograph_advantage,imp,inbox_tray,incoming_envelope,information_desk_person,information_source,innocent,' + + 'interrobang,iphone,it,izakaya_lantern,jack_o_lantern,japan,japanese_castle,japanese_goblin,japanese_ogre,jeans,' + + 'joy,joy_cat,jp,key,keycap_ten,kimono,kiss,kissing,kissing_cat,kissing_closed_eyes,kissing_face,kissing_heart,' + + 'kissing_smiling_eyes,koala,koko,kr,large_blue_circle,large_blue_diamond,large_orange_diamond,last_quarter_moon,' + + 'last_quarter_moon_with_face,laughing,leaves,ledger,left_luggage,left_right_arrow,leftwards_arrow_with_hook,' + + 'lemon,leo,leopard,libra,light_rail,link,lips,lipstick,lock,lock_with_ink_pen,lollipop,loop,loudspeaker,' + + 'love_hotel,love_letter,low_brightness,m,mag,mag_right,mahjong,mailbox,mailbox_closed,mailbox_with_mail,' + + 'mailbox_with_no_mail,man,man_with_gua_pi_mao,man_with_turban,mans_shoe,maple_leaf,mask,massage,meat_on_bone,' + + 'mega,melon,memo,mens,metal,metro,microphone,microscope,milky_way,minibus,minidisc,mobile_phone_off,' + + 'money_with_wings,moneybag,monkey,monkey_face,monorail,mortar_board,mount_fuji,mountain_bicyclist,' + + 'mountain_cableway,mountain_railway,mouse,mouse2,movie_camera,moyai,muscle,mushroom,musical_keyboard,' + + 'musical_note,musical_score,mute,nail_care,name_badge,neckbeard,necktie,negative_squared_cross_mark,' + + 'neutral_face,new,new_moon,new_moon_with_face,newspaper,ng,nine,no_bell,no_bicycles,no_entry,no_entry_sign,' + + 'no_good,no_mobile_phones,no_mouth,no_pedestrians,no_smoking,non-potable_water,nose,notebook,' + + 'notebook_with_decorative_cover,notes,nut_and_bolt,o,o2,ocean,octocat,octopus,oden,office,ok,ok_hand,' + + 'ok_woman,older_man,older_woman,on,oncoming_automobile,oncoming_bus,oncoming_police_car,oncoming_taxi,one,' + + 'open_file_folder,open_hands,open_mouth,ophiuchus,orange_book,outbox_tray,ox,package,page_facing_up,' + + 'page_with_curl,pager,palm_tree,panda_face,paperclip,parking,part_alternation_mark,partly_sunny,' + + 'passport_control,paw_prints,peach,pear,pencil,pencil2,penguin,pensive,performing_arts,persevere,' + + 'person_frowning,person_with_blond_hair,person_with_pouting_face,phone,pig,pig2,pig_nose,pill,pineapple,pisces,' + + 'pizza,plus1,point_down,point_left,point_right,point_up,point_up_2,police_car,poodle,poop,post_office,' + + 'postal_horn,postbox,potable_water,pouch,poultry_leg,pound,pouting_cat,pray,princess,punch,purple_heart,purse,' + + 'pushpin,put_litter_in_its_place,question,rabbit,rabbit2,racehorse,radio,radio_button,rage,rage1,rage2,rage3,' + + 'rage4,railway_car,rainbow,raised_hand,raised_hands,raising_hand,ram,ramen,rat,recycle,red_car,red_circle,' + + 'registered,relaxed,relieved,repeat,repeat_one,restroom,revolving_hearts,rewind,ribbon,rice,rice_ball,' + + 'rice_cracker,rice_scene,ring,rocket,roller_coaster,rooster,rose,rotating_light,round_pushpin,rowboat,ru,' + + 'rugby_football,runner,running,running_shirt_with_sash,sa,sagittarius,sailboat,sake,sandal,santa,satellite,' + + 'satisfied,saxophone,school,school_satchel,scissors,scorpius,scream,scream_cat,scroll,seat,secret,see_no_evil,' + + 'seedling,seven,shaved_ice,sheep,shell,ship,shipit,shirt,shit,shoe,shower,signal_strength,six,six_pointed_star,' + + 'ski,skull,sleeping,sleepy,slot_machine,small_blue_diamond,small_orange_diamond,small_red_triangle,' + + 'small_red_triangle_down,smile,smile_cat,smiley,smiley_cat,smiling_imp,smirk,smirk_cat,smoking,snail,snake,' + + 'snowboarder,snowflake,snowman,sob,soccer,soon,sos,sound,space_invader,spades,spaghetti,sparkle,sparkler,' + + 'sparkles,sparkling_heart,speak_no_evil,speaker,speech_balloon,speedboat,squirrel,star,star2,stars,station,' + + 'statue_of_liberty,steam_locomotive,stew,straight_ruler,strawberry,stuck_out_tongue,stuck_out_tongue_closed_eyes,' + + 'stuck_out_tongue_winking_eye,sun_with_face,sunflower,sunglasses,sunny,sunrise,sunrise_over_mountains,surfer,' + + 'sushi,suspect,suspension_railway,sweat,sweat_drops,sweat_smile,sweet_potato,swimmer,symbols,syringe,tada,' + + 'tanabata_tree,tangerine,taurus,taxi,tea,telephone,telephone_receiver,telescope,tennis,tent,thought_balloon,' + + 'three,thumbsdown,thumbsup,ticket,tiger,tiger2,tired_face,tm,toilet,tokyo_tower,tomato,tongue,top,tophat,' + + 'tractor,traffic_light,train,train2,tram,triangular_flag_on_post,triangular_ruler,trident,triumph,trolleybus,' + + 'trollface,trophy,tropical_drink,tropical_fish,truck,trumpet,tshirt,tulip,turtle,tv,twisted_rightwards_arrows,' + + 'two,two_hearts,two_men_holding_hands,two_women_holding_hands,u5272,u5408,u55b6,u6307,u6708,u6709,u6e80,u7121,' + + 'u7533,u7981,u7a7a,uk,umbrella,unamused,underage,unlock,up,us,v,vertical_traffic_light,vhs,vibration_mode,' + + 'video_camera,video_game,violin,virgo,volcano,vs,walking,waning_crescent_moon,waning_gibbous_moon,warning,watch,' + + 'water_buffalo,watermelon,wave,wavy_dash,waxing_crescent_moon,waxing_gibbous_moon,wc,weary,wedding,whale,whale2,' + + 'wheelchair,white_check_mark,white_circle,white_flower,white_large_square,white_medium_small_square,' + + 'white_medium_square,white_small_square,white_square_button,wind_chime,wine_glass,wink,wolf,woman,' + + 'womans_clothes,womans_hat,womens,worried,wrench,x,yellow_heart,yen,yum,zap,zero,zzz').split(','); + + // use a map to help make lookups faster instead of having to use indexOf on an array + const out = new Map(); + + for (let i = 0; i < emoticonNames.length; i++) { + out[emoticonNames[i]] = true; + } + + return out; +} + +const emoticonMap = initializeEmoticonMap(); + +export function handleEmoticons(text, tokens) { + let output = text; + + function replaceEmoticonWithToken(match, name) { + if (emoticonMap[name]) { + const index = tokens.size; + const alias = `MM_EMOTICON${index}`; + + tokens.set(alias, { + value: `<img align="absmiddle" alt=${match} class="emoji" src=${getImagePathForEmoticon(name)} title=${match} />`, + originalText: match + }); + + return alias; + } + + return match; + } + + output = output.replace(/:([a-zA-Z0-9_-]+):/g, replaceEmoticonWithToken); + + $.each(emoticonPatterns, (name, pattern) => { + // this might look a bit funny, but since the name isn't contained in the actual match + // like with the named emoticons, we need to add it in manually + output = output.replace(pattern, (match) => replaceEmoticonWithToken(match, name)); + }); + + return output; +} + +function getImagePathForEmoticon(name) { + return `/static/images/emoji/${name}.png`; +} diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx index 96da54217..0a876a3e3 100644 --- a/web/react/utils/markdown.jsx +++ b/web/react/utils/markdown.jsx @@ -1,9 +1,18 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. +const TextFormatting = require('./text_formatting.jsx'); + const marked = require('marked'); export class MattermostMarkdownRenderer extends marked.Renderer { + constructor(options, formattingOptions = {}) { + super(options); + + this.text = this.text.bind(this); + + this.formattingOptions = formattingOptions; + } link(href, title, text) { let outHref = href; @@ -19,4 +28,8 @@ export class MattermostMarkdownRenderer extends marked.Renderer { return output; } + + text(text) { + return TextFormatting.doFormatText(text, this.formattingOptions); + } } diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx index 4e390f708..56bf49c3f 100644 --- a/web/react/utils/text_formatting.jsx +++ b/web/react/utils/text_formatting.jsx @@ -3,41 +3,58 @@ const Autolinker = require('autolinker'); const Constants = require('./constants.jsx'); +const Emoticons = require('./emoticons.jsx'); const Markdown = require('./markdown.jsx'); const UserStore = require('../stores/user_store.jsx'); const Utils = require('./utils.jsx'); const marked = require('marked'); -const markdownRenderer = new Markdown.MattermostMarkdownRenderer(); - // Performs formatting of user posts including highlighting mentions and search terms and converting urls, hashtags, and // @mentions to links by taking a user's message and returning a string of formatted html. Also takes a number of options // as part of the second parameter: // - searchTerm - If specified, this word is highlighted in the resulting html. Defaults to nothing. // - mentionHighlight - Specifies whether or not to highlight mentions of the current user. Defaults to true. // - singleline - Specifies whether or not to remove newlines. Defaults to false. +// - emoticons - Enables emoticon parsing. Defaults to true. // - markdown - Enables markdown parsing. Defaults to true. export function formatText(text, options = {}) { - if (!('markdown' in options)) { - options.markdown = true; - } - - // wait until marked can sanitize the html so that we don't break markdown block quotes let output; - if (!options.markdown) { - output = sanitizeHtml(text); + + if (!('markdown' in options) || options.markdown) { + // the markdown renderer will call doFormatText as necessary so just call marked + output = marked(text, { + renderer: new Markdown.MattermostMarkdownRenderer(null, options), + sanitize: true + }); } else { - output = text; + output = sanitizeHtml(text); + output = doFormatText(output, options); + } + + // replace newlines with spaces if necessary + if (options.singleline) { + output = replaceNewlines(output); } + return output; +} + +// Performs most of the actual formatting work for formatText. Not intended to be called normally. +export function doFormatText(text, options) { + let output = text; + const tokens = new Map(); // replace important words and phrases with tokens - output = autolinkUrls(output, tokens, !!options.markdown); + output = autolinkUrls(output, tokens); output = autolinkAtMentions(output, tokens); output = autolinkHashtags(output, tokens); + if (!('emoticons' in options) || options.emoticon) { + output = Emoticons.handleEmoticons(output, tokens); + } + if (options.searchTerm) { output = highlightSearchTerm(output, tokens, options.searchTerm); } @@ -46,22 +63,9 @@ export function formatText(text, options = {}) { output = highlightCurrentMentions(output, tokens); } - // perform markdown parsing while we have an html-free input string - if (options.markdown) { - output = marked(output, { - renderer: markdownRenderer, - sanitize: true - }); - } - // reinsert tokens with formatted versions of the important words and phrases output = replaceTokens(output, tokens); - // replace newlines with html line breaks - if (options.singleline) { - output = replaceNewlines(output); - } - return output; } @@ -78,7 +82,7 @@ export function sanitizeHtml(text) { return output; } -function autolinkUrls(text, tokens, markdown) { +function autolinkUrls(text, tokens) { function replaceUrlWithToken(autolinker, match) { const linkText = match.getMatchedText(); let url = linkText; @@ -108,30 +112,7 @@ function autolinkUrls(text, tokens, markdown) { replaceFn: replaceUrlWithToken }); - let output = text; - - // temporarily replace markdown links if markdown is enabled so that we don't accidentally parse them twice - const markdownLinkTokens = new Map(); - if (markdown) { - function replaceMarkdownLinkWithToken(markdownLink) { - const index = markdownLinkTokens.size; - const alias = `MM_MARKDOWNLINK${index}`; - - markdownLinkTokens.set(alias, {value: markdownLink}); - - return alias; - } - - output = output.replace(/\]\([^\)]*\)/g, replaceMarkdownLinkWithToken); - } - - output = autolinker.link(output); - - if (markdown) { - output = replaceTokens(output, markdownLinkTokens); - } - - return output; + return autolinker.link(text); } function autolinkAtMentions(text, tokens) { @@ -241,7 +222,7 @@ function autolinkHashtags(text, tokens) { return prefix + alias; } - return output.replace(/(^|\W)(#[a-zA-Z0-9.\-_]+)\b/g, replaceHashtagWithToken); + return output.replace(/(^|\W)(#[a-zA-Z][a-zA-Z0-9.\-_]*)\b/g, replaceHashtagWithToken); } function highlightSearchTerm(text, tokens, searchTerm) { diff --git a/web/static/js/emojify.min.js b/web/static/js/emojify.min.js deleted file mode 100755 index 4fedf3205..000000000 --- a/web/static/js/emojify.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/*! emojify.js - v1.0.5 - - * Copyright (c) Hassan Khan 2015 - */ -!function(e,a){"use strict";"function"==typeof define&&define.amd?define([],a):"object"==typeof exports?module.exports=a():e.emojify=a()}(this,function(){"use strict";var e=function(){function e(){var e={named:/:([a-z0-9A-Z_-]+):/,smile:/:-?\)/g,open_mouth:/:o/gi,scream:/:-o/gi,smirk:/[:;]-?]/g,grinning:/[:;]-?d/gi,stuck_out_tongue_closed_eyes:/x-d/gi,stuck_out_tongue_winking_eye:/[:;]-?p/gi,rage:/:-?[\[@]/g,frowning:/:-?\(/g,sob:/:['’]-?\(|:'\(/g,kissing_heart:/:-?\*/g,wink:/;-?\)/g,pensive:/:-?\//g,confounded:/:-?s/gi,flushed:/:-?\|/g,relaxed:/:-?\$/g,mask:/:-x/gi,heart:/<3|<3/g,broken_heart:/<\/3|</3/g,thumbsup:/:\+1:/g,thumbsdown:/:\-1:/g};return d.ignore_emoticons&&(e={named:/:([a-z0-9A-Z_-]+):/,thumbsup:/:\+1:/g,thumbsdown:/:\-1:/g}),Object.keys(e).map(function(a){return[e[a],a]})}function a(){var e=_.map(function(e){var a=e[0],o=a.source||a;return o=o.replace(/(^|[^\[])\^/g,"$1"),"("+o+")"}).join("|");return new RegExp(e,"gi")}function o(e){return" "===e||" "===e||"\r"===e||"\n"===e||""===e||e===String.fromCharCode(160)}function r(e){var a=null;if(e.replacer)a=e.replacer.apply({config:d},[":"+e.emojiName+":",e.emojiName]);else{var o=d.tag_type||h[d.mode];a=e.win.document.createElement(o),"img"!==o?a.setAttribute("class","emoji emoji-"+e.emojiName):(a.setAttribute("align","absmiddle"),a.setAttribute("alt",":"+e.emojiName+":"),a.setAttribute("class","emoji"),a.setAttribute("src",d.img_dir+"/"+e.emojiName+".png")),a.setAttribute("title",":"+e.emojiName+":")}e.node.splitText(e.match.index),e.node.nextSibling.nodeValue=e.node.nextSibling.nodeValue.substr(e.match[0].length,e.node.nextSibling.nodeValue.length),a.appendChild(e.node.splitText(e.match.index)),e.node.parentNode.insertBefore(a,e.node.nextSibling)}function t(e){if(e[1]&&e[2]){var a=e[2];if(m[a])return a}else for(var o=3;o<e.length-1;o++)if(e[o])return _[o-2][1]}function i(e,a){var o=this.config.tag_type||h[this.config.mode];return"img"!==o?"<"+o+" class='emoji emoji-"+a+"' title=':"+a+":'></"+o+">":"<img align='absmiddle' alt=':"+a+":' class='emoji' src='"+this.config.img_dir+"/"+a+".png' title=':"+a+":' />"}function n(){this.lastEmojiTerminatedAt=-1}function s(o,r){if(!o)return o;r||(r=i),_=e(),c=a();var t=new n;return o.replace(c,function(){var e=Array.prototype.slice.call(arguments,0,-2),a=arguments[arguments.length-2],o=arguments[arguments.length-1],i=t.validate(e,a,o);return i?r.apply({config:d},[arguments[0],i]):arguments[0]})}function l(o,i){"undefined"==typeof o&&(o=d.only_crawl_id?document.getElementById(d.only_crawl_id):document.body);var s=o.ownerDocument,l=s.defaultView||s.parentWindow,u=function(e,a){var o;if(e.hasChildNodes())for(o=e.firstChild;o;)a(o)&&u(o,a),o=o.nextSibling},g=function(e){for(var a,o=[],s=new n;null!==(a=c.exec(e.data));)s.validate(a,a.index,a.input)&&o.push(a);for(var _=o.length;_-->0;){var u=t(o[_]);r({node:e,match:o[_],emojiName:u,replacer:i,win:l})}};_=e(),c=a();var m=[],h=new RegExp(d.blacklist.elements.join("|"),"i"),p=new RegExp(d.blacklist.classes.join("|"),"i");if("undefined"!=typeof l.document.createTreeWalker)for(var b,f=l.document.createTreeWalker(o,l.NodeFilter.SHOW_TEXT|l.NodeFilter.SHOW_ELEMENT,function(e){return 1!==e.nodeType?l.NodeFilter.FILTER_ACCEPT:e.tagName.match(h)||"svg"===e.tagName||e.className.match(p)?l.NodeFilter.FILTER_REJECT:l.NodeFilter.FILTER_SKIP},!1);null!==(b=f.nextNode());)m.push(b);else u(o,function(e){return"undefined"!=typeof e.tagName&&e.tagName.match(h)||"undefined"!=typeof e.className&&e.className.match(p)?!1:1===e.nodeType?!0:(m.push(e),!0)});m.forEach(g)}var _,c,u="+1,-1,100,1234,8ball,a,ab,abc,abcd,accept,aerial_tramway,airplane,alarm_clock,alien,ambulance,anchor,angel,anger,angry,anguished,ant,apple,aquarius,aries,arrow_backward,arrow_double_down,arrow_double_up,arrow_down,arrow_down_small,arrow_forward,arrow_heading_down,arrow_heading_up,arrow_left,arrow_lower_left,arrow_lower_right,arrow_right,arrow_right_hook,arrow_up,arrow_up_down,arrow_up_small,arrow_upper_left,arrow_upper_right,arrows_clockwise,arrows_counterclockwise,art,articulated_lorry,astonished,atm,b,baby,baby_bottle,baby_chick,baby_symbol,back,baggage_claim,balloon,ballot_box_with_check,bamboo,banana,bangbang,bank,bar_chart,barber,baseball,basketball,bath,bathtub,battery,bear,bee,beer,beers,beetle,beginner,bell,bento,bicyclist,bike,bikini,bird,birthday,black_circle,black_joker,black_medium_small_square,black_medium_square,black_nib,black_small_square,black_square,black_square_button,blossom,blowfish,blue_book,blue_car,blue_heart,blush,boar,boat,bomb,book,bookmark,bookmark_tabs,books,boom,boot,bouquet,bow,bowling,bowtie,boy,bread,bride_with_veil,bridge_at_night,briefcase,broken_heart,bug,bulb,bullettrain_front,bullettrain_side,bus,busstop,bust_in_silhouette,busts_in_silhouette,cactus,cake,calendar,calling,camel,camera,cancer,candy,capital_abcd,capricorn,car,card_index,carousel_horse,cat,cat2,cd,chart,chart_with_downwards_trend,chart_with_upwards_trend,checkered_flag,cherries,cherry_blossom,chestnut,chicken,children_crossing,chocolate_bar,christmas_tree,church,cinema,circus_tent,city_sunrise,city_sunset,cl,clap,clapper,clipboard,clock1,clock10,clock1030,clock11,clock1130,clock12,clock1230,clock130,clock2,clock230,clock3,clock330,clock4,clock430,clock5,clock530,clock6,clock630,clock7,clock730,clock8,clock830,clock9,clock930,closed_book,closed_lock_with_key,closed_umbrella,cloud,clubs,cn,cocktail,coffee,cold_sweat,collision,computer,confetti_ball,confounded,confused,congratulations,construction,construction_worker,convenience_store,cookie,cool,cop,copyright,corn,couple,couple_with_heart,couplekiss,cow,cow2,credit_card,crescent_moon,crocodile,crossed_flags,crown,cry,crying_cat_face,crystal_ball,cupid,curly_loop,currency_exchange,curry,custard,customs,cyclone,dancer,dancers,dango,dart,dash,date,de,deciduous_tree,department_store,diamond_shape_with_a_dot_inside,diamonds,disappointed,disappointed_relieved,dizzy,dizzy_face,do_not_litter,dog,dog2,dollar,dolls,dolphin,donut,door,doughnut,dragon,dragon_face,dress,dromedary_camel,droplet,dvd,e-mail,ear,ear_of_rice,earth_africa,earth_americas,earth_asia,egg,eggplant,eight,eight_pointed_black_star,eight_spoked_asterisk,electric_plug,elephant,email,end,envelope,es,euro,european_castle,european_post_office,evergreen_tree,exclamation,expressionless,eyeglasses,eyes,facepunch,factory,fallen_leaf,family,fast_forward,fax,fearful,feelsgood,feet,ferris_wheel,file_folder,finnadie,fire,fire_engine,fireworks,first_quarter_moon,first_quarter_moon_with_face,fish,fish_cake,fishing_pole_and_fish,fist,five,flags,flashlight,floppy_disk,flower_playing_cards,flushed,foggy,football,fork_and_knife,fountain,four,four_leaf_clover,fr,free,fried_shrimp,fries,frog,frowning,fu,fuelpump,full_moon,full_moon_with_face,game_die,gb,gem,gemini,ghost,gift,gift_heart,girl,globe_with_meridians,goat,goberserk,godmode,golf,grapes,green_apple,green_book,green_heart,grey_exclamation,grey_question,grimacing,grin,grinning,guardsman,guitar,gun,haircut,hamburger,hammer,hamster,hand,handbag,hankey,hash,hatched_chick,hatching_chick,headphones,hear_no_evil,heart,heart_decoration,heart_eyes,heart_eyes_cat,heartbeat,heartpulse,hearts,heavy_check_mark,heavy_division_sign,heavy_dollar_sign,heavy_exclamation_mark,heavy_minus_sign,heavy_multiplication_x,heavy_plus_sign,helicopter,herb,hibiscus,high_brightness,high_heel,hocho,honey_pot,honeybee,horse,horse_racing,hospital,hotel,hotsprings,hourglass,hourglass_flowing_sand,house,house_with_garden,hurtrealbad,hushed,ice_cream,icecream,id,ideograph_advantage,imp,inbox_tray,incoming_envelope,information_desk_person,information_source,innocent,interrobang,iphone,it,izakaya_lantern,jack_o_lantern,japan,japanese_castle,japanese_goblin,japanese_ogre,jeans,joy,joy_cat,jp,key,keycap_ten,kimono,kiss,kissing,kissing_cat,kissing_closed_eyes,kissing_face,kissing_heart,kissing_smiling_eyes,koala,koko,kr,large_blue_circle,large_blue_diamond,large_orange_diamond,last_quarter_moon,last_quarter_moon_with_face,laughing,leaves,ledger,left_luggage,left_right_arrow,leftwards_arrow_with_hook,lemon,leo,leopard,libra,light_rail,link,lips,lipstick,lock,lock_with_ink_pen,lollipop,loop,loudspeaker,love_hotel,love_letter,low_brightness,m,mag,mag_right,mahjong,mailbox,mailbox_closed,mailbox_with_mail,mailbox_with_no_mail,man,man_with_gua_pi_mao,man_with_turban,mans_shoe,maple_leaf,mask,massage,meat_on_bone,mega,melon,memo,mens,metal,metro,microphone,microscope,milky_way,minibus,minidisc,mobile_phone_off,money_with_wings,moneybag,monkey,monkey_face,monorail,mortar_board,mount_fuji,mountain_bicyclist,mountain_cableway,mountain_railway,mouse,mouse2,movie_camera,moyai,muscle,mushroom,musical_keyboard,musical_note,musical_score,mute,nail_care,name_badge,neckbeard,necktie,negative_squared_cross_mark,neutral_face,new,new_moon,new_moon_with_face,newspaper,ng,nine,no_bell,no_bicycles,no_entry,no_entry_sign,no_good,no_mobile_phones,no_mouth,no_pedestrians,no_smoking,non-potable_water,nose,notebook,notebook_with_decorative_cover,notes,nut_and_bolt,o,o2,ocean,octocat,octopus,oden,office,ok,ok_hand,ok_woman,older_man,older_woman,on,oncoming_automobile,oncoming_bus,oncoming_police_car,oncoming_taxi,one,open_file_folder,open_hands,open_mouth,ophiuchus,orange_book,outbox_tray,ox,package,page_facing_up,page_with_curl,pager,palm_tree,panda_face,paperclip,parking,part_alternation_mark,partly_sunny,passport_control,paw_prints,peach,pear,pencil,pencil2,penguin,pensive,performing_arts,persevere,person_frowning,person_with_blond_hair,person_with_pouting_face,phone,pig,pig2,pig_nose,pill,pineapple,pisces,pizza,plus1,point_down,point_left,point_right,point_up,point_up_2,police_car,poodle,poop,post_office,postal_horn,postbox,potable_water,pouch,poultry_leg,pound,pouting_cat,pray,princess,punch,purple_heart,purse,pushpin,put_litter_in_its_place,question,rabbit,rabbit2,racehorse,radio,radio_button,rage,rage1,rage2,rage3,rage4,railway_car,rainbow,raised_hand,raised_hands,raising_hand,ram,ramen,rat,recycle,red_car,red_circle,registered,relaxed,relieved,repeat,repeat_one,restroom,revolving_hearts,rewind,ribbon,rice,rice_ball,rice_cracker,rice_scene,ring,rocket,roller_coaster,rooster,rose,rotating_light,round_pushpin,rowboat,ru,rugby_football,runner,running,running_shirt_with_sash,sa,sagittarius,sailboat,sake,sandal,santa,satellite,satisfied,saxophone,school,school_satchel,scissors,scorpius,scream,scream_cat,scroll,seat,secret,see_no_evil,seedling,seven,shaved_ice,sheep,shell,ship,shipit,shirt,shit,shoe,shower,signal_strength,six,six_pointed_star,ski,skull,sleeping,sleepy,slot_machine,small_blue_diamond,small_orange_diamond,small_red_triangle,small_red_triangle_down,smile,smile_cat,smiley,smiley_cat,smiling_imp,smirk,smirk_cat,smoking,snail,snake,snowboarder,snowflake,snowman,sob,soccer,soon,sos,sound,space_invader,spades,spaghetti,sparkle,sparkler,sparkles,sparkling_heart,speak_no_evil,speaker,speech_balloon,speedboat,squirrel,star,star2,stars,station,statue_of_liberty,steam_locomotive,stew,straight_ruler,strawberry,stuck_out_tongue,stuck_out_tongue_closed_eyes,stuck_out_tongue_winking_eye,sun_with_face,sunflower,sunglasses,sunny,sunrise,sunrise_over_mountains,surfer,sushi,suspect,suspension_railway,sweat,sweat_drops,sweat_smile,sweet_potato,swimmer,symbols,syringe,tada,tanabata_tree,tangerine,taurus,taxi,tea,telephone,telephone_receiver,telescope,tennis,tent,thought_balloon,three,thumbsdown,thumbsup,ticket,tiger,tiger2,tired_face,tm,toilet,tokyo_tower,tomato,tongue,top,tophat,tractor,traffic_light,train,train2,tram,triangular_flag_on_post,triangular_ruler,trident,triumph,trolleybus,trollface,trophy,tropical_drink,tropical_fish,truck,trumpet,tshirt,tulip,turtle,tv,twisted_rightwards_arrows,two,two_hearts,two_men_holding_hands,two_women_holding_hands,u5272,u5408,u55b6,u6307,u6708,u6709,u6e80,u7121,u7533,u7981,u7a7a,uk,umbrella,unamused,underage,unlock,up,us,v,vertical_traffic_light,vhs,vibration_mode,video_camera,video_game,violin,virgo,volcano,vs,walking,waning_crescent_moon,waning_gibbous_moon,warning,watch,water_buffalo,watermelon,wave,wavy_dash,waxing_crescent_moon,waxing_gibbous_moon,wc,weary,wedding,whale,whale2,wheelchair,white_check_mark,white_circle,white_flower,white_large_square,white_medium_small_square,white_medium_square,white_small_square,white_square_button,wind_chime,wine_glass,wink,wolf,woman,womans_clothes,womans_hat,womens,worried,wrench,x,yellow_heart,yen,yum,zap,zero,zzz",g=u.split(/,/),m=g.reduce(function(e,a){return e[a]=!0,e},{}),d={blacklist:{ids:[],classes:["no-emojify"],elements:["script","textarea","a","pre","code"]},tag_type:null,only_crawl_id:null,img_dir:"images/emoji",ignore_emoticons:!1,mode:"img"},h={img:"img",sprite:"span","data-uri":"span"};return n.prototype={validate:function(e,a,r){function i(){return n.lastEmojiTerminatedAt=_+a,s}var n=this,s=t(e);if(s){var l=e[0],_=l.length;if(0===a)return i();if(r.length===l.length+a)return i();var c=this.lastEmojiTerminatedAt===a;if(c)return i();if(o(r.charAt(a-1)))return i();var u=o(r.charAt(l.length+a));return u&&c?i():void 0}}},{defaultConfig:d,emojiNames:g,setConfig:function(e){Object.keys(d).forEach(function(a){a in e&&(d[a]=e[a])})},replace:s,run:l}}();return e});
\ No newline at end of file diff --git a/web/templates/head.html b/web/templates/head.html index af5c86bba..a3ac930e9 100644 --- a/web/templates/head.html +++ b/web/templates/head.html @@ -40,11 +40,6 @@ <script src="/static/js/jquery-dragster/jquery.dragster.js"></script> - <script src="/static/js/emojify.min.js"></script> - <script> - emojify.setConfig({img_dir: '/static/images/emoji'}); - </script> - <style id="antiClickjack">body{display:none !important;}</style> <script src="/static/js/bundle.js"></script> <script type="text/javascript"> diff --git a/web/web.go b/web/web.go index 305e4f199..0cf45766e 100644 --- a/web/web.go +++ b/web/web.go @@ -9,11 +9,13 @@ import ( "github.com/gorilla/mux" "github.com/mattermost/platform/api" "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" "github.com/mssola/user_agent" "gopkg.in/fsnotify.v1" "html/template" "net/http" + "regexp" "strconv" "strings" ) @@ -63,6 +65,8 @@ func InitWeb() { mainrouter.Handle("/admin_console", api.UserRequired(adminConsole)).Methods("GET") + mainrouter.Handle("/hooks/{id:[A-Za-z0-9]+}", api.ApiAppHandler(incomingWebhook)).Methods("POST") + // ---------------------------------------------------------------------------------------------- // *ANYTHING* team specific should go below this line // ---------------------------------------------------------------------------------------------- @@ -355,6 +359,7 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) { resend := r.URL.Query().Get("resend") + resendSuccess := r.URL.Query().Get("resend_success") name := r.URL.Query().Get("teamname") email := r.URL.Query().Get("email") hashedId := r.URL.Query().Get("hid") @@ -375,7 +380,9 @@ func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) { } else { user := result.Data.(*model.User) api.FireAndForgetVerifyEmail(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) - http.Redirect(w, r, "/", http.StatusFound) + + newAddress := strings.Replace(r.URL.String(), "&resend=true", "&resend_success=true", -1) + http.Redirect(w, r, newAddress, http.StatusFound) return } } @@ -400,6 +407,7 @@ func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) { page.Props["IsVerified"] = isVerified page.Props["TeamURL"] = c.GetTeamURLFromTeam(team) page.Props["UserEmail"] = email + page.Props["ResendSuccess"] = resendSuccess page.Render(c, w) } @@ -838,3 +846,84 @@ func getAccessToken(c *api.Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(accessRsp.ToJson())) } + +func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + id := params["id"] + + hchan := api.Srv.Store.Webhook().GetIncoming(id) + + r.ParseForm() + + props := model.MapFromJson(strings.NewReader(r.FormValue("payload"))) + + text := props["text"] + if len(text) == 0 { + c.Err = model.NewAppError("incomingWebhook", "No text specified", "") + return + } + + channelName := props["channel"] + + var hook *model.IncomingWebhook + if result := <-hchan; result.Err != nil { + c.Err = model.NewAppError("incomingWebhook", "Invalid webhook", "err="+result.Err.Message) + return + } else { + hook = result.Data.(*model.IncomingWebhook) + } + + var channel *model.Channel + var cchan store.StoreChannel + + if len(channelName) != 0 { + if channelName[0] == '@' { + if result := <-api.Srv.Store.User().GetByUsername(hook.TeamId, channelName[1:]); result.Err != nil { + c.Err = model.NewAppError("incomingWebhook", "Couldn't find the user", "err="+result.Err.Message) + return + } else { + channelName = model.GetDMNameFromIds(result.Data.(*model.User).Id, hook.UserId) + } + } else if channelName[0] == '#' { + channelName = channelName[1:] + } + + cchan = api.Srv.Store.Channel().GetByName(hook.TeamId, channelName) + } else { + cchan = api.Srv.Store.Channel().Get(hook.ChannelId) + } + + // parse links into Markdown format + linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`) + text = linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})") + + linkRegex := regexp.MustCompile(`<\s*(\S*)\s*>`) + text = linkRegex.ReplaceAllString(text, "${1}") + + if result := <-cchan; result.Err != nil { + c.Err = model.NewAppError("incomingWebhook", "Couldn't find the channel", "err="+result.Err.Message) + return + } else { + channel = result.Data.(*model.Channel) + } + + pchan := api.Srv.Store.Channel().CheckPermissionsTo(hook.TeamId, channel.Id, hook.UserId) + + post := &model.Post{UserId: hook.UserId, ChannelId: channel.Id, Message: text} + + if !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN { + c.Err = model.NewAppError("incomingWebhook", "Inappropriate channel permissions", "") + return + } + + // create a mock session + c.Session = model.Session{UserId: hook.UserId, TeamId: hook.TeamId, IsOAuth: false} + + if _, err := api.CreatePost(c, post, false); err != nil { + c.Err = model.NewAppError("incomingWebhook", "Error creating post", "err="+err.Message) + return + } + + w.Header().Set("Content-Type", "text/plain") + w.Write([]byte("ok")) +} diff --git a/web/web_test.go b/web/web_test.go index 3da7eb2dc..1cb1c0a34 100644 --- a/web/web_test.go +++ b/web/web_test.go @@ -180,6 +180,51 @@ func TestGetAccessToken(t *testing.T) { } } +func TestIncomingWebhook(t *testing.T) { + Setup() + + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = ApiClient.Must(ApiClient.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user = ApiClient.Must(ApiClient.CreateUser(user, "")).Data.(*model.User) + store.Must(api.Srv.Store.User().VerifyEmail(user.Id)) + + ApiClient.LoginByEmail(team.Name, user.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = ApiClient.Must(ApiClient.CreateChannel(channel1)).Data.(*model.Channel) + + if utils.Cfg.ServiceSettings.AllowIncomingWebhooks { + hook1 := &model.IncomingWebhook{ChannelId: channel1.Id} + hook1 = ApiClient.Must(ApiClient.CreateIncomingWebhook(hook1)).Data.(*model.IncomingWebhook) + + payload := "payload={\"text\": \"test text\"}" + if _, err := ApiClient.PostToWebhook(hook1.Id, payload); err != nil { + t.Fatal(err) + } + + payload = "payload={\"text\": \"\"}" + if _, err := ApiClient.PostToWebhook(hook1.Id, payload); err == nil { + t.Fatal("should have errored - no text to post") + } + + payload = "payload={\"text\": \"test text\", \"channel\": \"junk\"}" + if _, err := ApiClient.PostToWebhook(hook1.Id, payload); err == nil { + t.Fatal("should have errored - bad channel") + } + + payload = "payload={\"text\": \"test text\"}" + if _, err := ApiClient.PostToWebhook("abc123", payload); err == nil { + t.Fatal("should have errored - bad hook") + } + } else { + if _, err := ApiClient.PostToWebhook("123", "123"); err == nil { + t.Fatal("should have failed - webhooks turned off") + } + } +} + func TestZZWebTearDown(t *testing.T) { // *IMPORTANT* // This should be the last function in any test file |