diff options
Diffstat (limited to 'webapp/components')
33 files changed, 1093 insertions, 228 deletions
diff --git a/webapp/components/admin_console/custom_emoji_settings.jsx b/webapp/components/admin_console/custom_emoji_settings.jsx index 332c7b216..738afa3cd 100644 --- a/webapp/components/admin_console/custom_emoji_settings.jsx +++ b/webapp/components/admin_console/custom_emoji_settings.jsx @@ -27,7 +27,10 @@ export default class CustomEmojiSettings extends AdminSettings { getConfigFromState(config) { config.ServiceSettings.EnableCustomEmoji = this.state.enableCustomEmoji; - config.ServiceSettings.RestrictCustomEmojiCreation = this.state.restrictCustomEmojiCreation; + + if (global.window.mm_license.IsLicensed === 'true') { + config.ServiceSettings.RestrictCustomEmojiCreation = this.state.restrictCustomEmojiCreation; + } return config; } @@ -44,29 +47,14 @@ export default class CustomEmojiSettings extends AdminSettings { } renderSettings() { - return ( - <SettingsGroup> - <BooleanSetting - id='enableCustomEmoji' - label={ - <FormattedMessage - id='admin.customization.enableCustomEmojiTitle' - defaultMessage='Enable Custom Emoji:' - /> - } - helpText={ - <FormattedMessage - id='admin.customization.enableCustomEmojiDesc' - defaultMessage='Enable users to create custom emoji for use in chat messages.' - /> - } - value={this.state.enableCustomEmoji} - onChange={this.handleChange} - /> + let restrictSetting = null; + if (global.window.mm_license.IsLicensed === 'true') { + restrictSetting = ( <DropdownSetting id='restrictCustomEmojiCreation' values={[ {value: 'all', text: Utils.localizeMessage('admin.customization.restrictCustomEmojiCreationAll', 'Allow everyone to create custom emoji')}, + {value: 'admin', text: Utils.localizeMessage('admin.customization.restrictCustomEmojiCreationAdmin', 'Allow system and team admins to create custom emoji')}, {value: 'system_admin', text: Utils.localizeMessage('admin.customization.restrictCustomEmojiCreationSystemAdmin', 'Only allow system admins to create custom emoji')} ]} label={ @@ -85,6 +73,29 @@ export default class CustomEmojiSettings extends AdminSettings { onChange={this.handleChange} disabled={!this.state.enableCustomEmoji} /> + ); + } + + return ( + <SettingsGroup> + <BooleanSetting + id='enableCustomEmoji' + label={ + <FormattedMessage + id='admin.customization.enableCustomEmojiTitle' + defaultMessage='Enable Custom Emoji:' + /> + } + helpText={ + <FormattedMessage + id='admin.customization.enableCustomEmojiDesc' + defaultMessage='Enable users to create custom emoji for use in chat messages.' + /> + } + value={this.state.enableCustomEmoji} + onChange={this.handleChange} + /> + {restrictSetting} </SettingsGroup> ); } diff --git a/webapp/components/backstage/backstage_controller.jsx b/webapp/components/backstage/backstage_controller.jsx new file mode 100644 index 000000000..690880071 --- /dev/null +++ b/webapp/components/backstage/backstage_controller.jsx @@ -0,0 +1,71 @@ +// 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 BackstageSidebar from './components/backstage_sidebar.jsx'; +import BackstageNavbar from './components/backstage_navbar.jsx'; +import ErrorBar from 'components/error_bar.jsx'; + +export default class BackstageController extends React.Component { + static get propTypes() { + return { + children: React.PropTypes.node.isRequired, + params: React.PropTypes.object.isRequired, + user: React.PropTypes.user.isRequired + }; + } + + constructor(props) { + super(props); + + this.onTeamChange = this.onTeamChange.bind(this); + + this.state = { + team: props.params.team ? TeamStore.getByName(props.params.team) : TeamStore.getCurrent() + }; + } + + componentDidMount() { + TeamStore.addChangeListener(this.onTeamChange); + } + + componentWillUnmount() { + TeamStore.removeChangeListener(this.onTeamChange); + } + + onTeamChange() { + this.state = { + team: this.props.params.team ? TeamStore.getByName(this.props.params.team) : TeamStore.getCurrent() + }; + } + + render() { + return ( + <div className='backstage'> + <ErrorBar/> + <BackstageNavbar team={this.state.team}/> + <div className='backstage-body'> + <BackstageSidebar + team={this.state.team} + user={this.props.user} + /> + { + React.Children.map(this.props.children, (child) => { + if (!child) { + return child; + } + + return React.cloneElement(child, { + team: this.state.team, + user: this.props.user + }); + }) + } + </div> + </div> + ); + } +}
\ No newline at end of file diff --git a/webapp/components/backstage/backstage_category.jsx b/webapp/components/backstage/components/backstage_category.jsx index 1d4b11ca3..74dcf3476 100644 --- a/webapp/components/backstage/backstage_category.jsx +++ b/webapp/components/backstage/components/backstage_category.jsx @@ -59,6 +59,7 @@ export default class BackstageCategory extends React.Component { to={link} className='category-title' activeClassName='category-title--active' + onlyActiveOnIndex={true} > <i className={'fa ' + icon}/> <span className='category-title__text'> diff --git a/webapp/components/backstage/backstage_header.jsx b/webapp/components/backstage/components/backstage_header.jsx index 37b4be349..37b4be349 100644 --- a/webapp/components/backstage/backstage_header.jsx +++ b/webapp/components/backstage/components/backstage_header.jsx diff --git a/webapp/components/backstage/components/backstage_list.jsx b/webapp/components/backstage/components/backstage_list.jsx new file mode 100644 index 000000000..81b8ec4d9 --- /dev/null +++ b/webapp/components/backstage/components/backstage_list.jsx @@ -0,0 +1,108 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import * as Utils from 'utils/utils.jsx'; + +import {Link} from 'react-router'; +import LoadingScreen from 'components/loading_screen.jsx'; + +export default class BackstageList extends React.Component { + static propTypes = { + children: React.PropTypes.node, + header: React.PropTypes.node.isRequired, + addLink: React.PropTypes.string, + addText: React.PropTypes.node, + emptyText: React.PropTypes.node, + loading: React.PropTypes.bool.isRequired, + searchPlaceholder: React.PropTypes.string + } + + static defaultProps = { + searchPlaceholder: Utils.localizeMessage('backstage.search', 'Search') + } + + constructor(props) { + super(props); + + this.updateFilter = this.updateFilter.bind(this); + + this.state = { + filter: '' + }; + } + + updateFilter(e) { + this.setState({ + filter: e.target.value + }); + } + + render() { + const filter = this.state.filter.toLowerCase(); + + let children; + if (this.props.loading) { + children = <LoadingScreen/>; + } else { + children = React.Children.map(this.props.children, (child) => { + return React.cloneElement(child, {filter}); + }); + + if (children.length === 0 && this.props.emptyText) { + children = ( + <span className='backstage-list__item backstage-list__empty'> + {this.props.emptyText} + </span> + ); + } + } + + let addLink = null; + if (this.props.addLink && this.props.addText) { + addLink = ( + <Link + className='add-link' + to={this.props.addLink} + > + <button + type='button' + className='btn btn-primary' + > + <span> + {this.props.addText} + </span> + </button> + </Link> + ); + } + + return ( + <div className='backstage-content'> + <div className='backstage-header'> + <h1> + {this.props.header} + </h1> + {addLink} + </div> + <div className='backstage-filters'> + <div className='backstage-filter__search'> + <i className='fa fa-search'></i> + <input + type='search' + className='form-control' + placeholder={this.props.searchPlaceholder} + value={this.state.filter} + onChange={this.updateFilter} + style={{flexGrow: 0, flexShrink: 0}} + /> + </div> + </div> + <div className='backstage-list'> + {children} + </div> + </div> + ); + } +} diff --git a/webapp/components/backstage/backstage_navbar.jsx b/webapp/components/backstage/components/backstage_navbar.jsx index 26ab44c87..7bccfc9f7 100644 --- a/webapp/components/backstage/backstage_navbar.jsx +++ b/webapp/components/backstage/components/backstage_navbar.jsx @@ -1,52 +1,28 @@ // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import $ from 'jquery'; - import React from 'react'; -import TeamStore from 'stores/team_store.jsx'; - import {FormattedMessage} from 'react-intl'; import {Link} from 'react-router/es6'; export default class BackstageNavbar extends React.Component { - constructor(props) { - super(props); - - this.handleChange = this.handleChange.bind(this); - - this.state = { - team: TeamStore.getCurrent() + static get propTypes() { + return { + team: React.propTypes.object.isRequired }; } - componentDidMount() { - TeamStore.addChangeListener(this.handleChange); - $('body').addClass('backstage'); - } - - componentWillUnmount() { - TeamStore.removeChangeListener(this.handleChange); - $('body').removeClass('backstage'); - } - - handleChange() { - this.setState({ - team: TeamStore.getCurrent() - }); - } - render() { - if (!this.state.team) { + if (!this.props.team) { return null; } return ( - <div className='backstage-navbar row'> + <div className='backstage-navbar'> <Link className='backstage-navbar__back' - to={`/${this.state.team.name}/channels/town-square`} + to={`/${this.props.team.name}/channels/town-square`} > <i className='fa fa-angle-left'/> <span> diff --git a/webapp/components/backstage/backstage_section.jsx b/webapp/components/backstage/components/backstage_section.jsx index c8b63af18..c8b63af18 100644 --- a/webapp/components/backstage/backstage_section.jsx +++ b/webapp/components/backstage/components/backstage_section.jsx diff --git a/webapp/components/backstage/backstage_sidebar.jsx b/webapp/components/backstage/components/backstage_sidebar.jsx index 4d8d8337d..a17d830b0 100644 --- a/webapp/components/backstage/backstage_sidebar.jsx +++ b/webapp/components/backstage/components/backstage_sidebar.jsx @@ -3,13 +3,51 @@ import React from 'react'; -import * as Utils from 'utils/utils.jsx'; +import TeamStore from 'stores/team_store.jsx'; + 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() { + static get propTypes() { + return { + team: React.PropTypes.object.isRequired, + user: React.PropTypes.object.isRequired + }; + } + + renderCustomEmoji() { + if (window.mm_config.EnableCustomEmoji !== 'true') { + return null; + } + + return ( + <BackstageCategory + name='emoji' + parentLink={'/' + this.props.team.name} + icon='fa-smile-o' + title={ + <FormattedMessage + id='backstage_sidebar.emoji' + defaultMessage='Custom Emoji' + /> + } + /> + ); + } + + renderIntegrations() { + if (window.mm_config.EnableIncomingWebhooks !== 'true' && + window.mm_config.EnableOutgoingWebhooks !== 'true' && + window.mm_config.EnableCommands !== 'true') { + return null; + } + + if (window.mm_config.RestrictCustomEmojiCreation !== 'all' && !TeamStore.isTeamAdmin(this.props.user.id, this.props.team.id)) { + return null; + } + let incomingWebhooks = null; if (window.mm_config.EnableIncomingWebhooks === 'true') { incomingWebhooks = ( @@ -56,23 +94,30 @@ export default class BackstageSidebar extends React.Component { } return ( + <BackstageCategory + name='integrations' + parentLink={'/' + this.props.team.name} + icon='fa-link' + title={ + <FormattedMessage + id='backstage_sidebar.integrations' + defaultMessage='Integrations' + /> + } + > + {incomingWebhooks} + {outgoingWebhooks} + {commands} + </BackstageCategory> + ); + } + + render() { + return ( <div className='backstage-sidebar'> <ul> - <BackstageCategory - name='integrations' - parentLink={'/' + Utils.getTeamNameFromUrl() + '/settings'} - icon='fa-link' - title={ - <FormattedMessage - id='backstage_sidebar.integrations' - defaultMessage='Integrations' - /> - } - > - {incomingWebhooks} - {outgoingWebhooks} - {commands} - </BackstageCategory> + {this.renderCustomEmoji()} + {this.renderIntegrations()} </ul> </div> ); diff --git a/webapp/components/backstage/installed_integrations.jsx b/webapp/components/backstage/installed_integrations.jsx deleted file mode 100644 index f6de8bc11..000000000 --- a/webapp/components/backstage/installed_integrations.jsx +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React from 'react'; - -import * as Utils from 'utils/utils.jsx'; - -import {Link} from 'react-router/es6'; -import LoadingScreen from 'components/loading_screen.jsx'; - -export default class InstalledIntegrations extends React.Component { - static get propTypes() { - return { - children: React.PropTypes.node, - header: React.PropTypes.node.isRequired, - addLink: React.PropTypes.string.isRequired, - addText: React.PropTypes.node.isRequired, - emptyText: React.PropTypes.node.isRequired, - loading: React.PropTypes.bool.isRequired - }; - } - - constructor(props) { - super(props); - - this.updateFilter = this.updateFilter.bind(this); - - this.state = { - filter: '' - }; - } - - updateFilter(e) { - this.setState({ - filter: e.target.value - }); - } - - render() { - const filter = this.state.filter.toLowerCase(); - - let children; - - if (this.props.loading) { - children = <LoadingScreen/>; - } else { - children = React.Children.map(this.props.children, (child) => { - return React.cloneElement(child, {filter}); - }); - - if (children.length === 0) { - children = ( - <span className='backstage-list__item backstage-list_empty'> - {this.props.emptyText} - </span> - ); - } - } - - return ( - <div className='backstage-content'> - <div className='installed-integrations'> - <div className='backstage-header'> - <h1> - {this.props.header} - </h1> - <Link - className='add-integrations-link' - to={this.props.addLink} - > - <button - type='button' - className='btn btn-primary' - > - <span> - {this.props.addText} - </span> - </button> - </Link> - </div> - <div className='backstage-filters'> - <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'> - {children} - </div> - </div> - </div> - ); - } -} diff --git a/webapp/components/emoji/components/add_emoji.jsx b/webapp/components/emoji/components/add_emoji.jsx new file mode 100644 index 000000000..46f345476 --- /dev/null +++ b/webapp/components/emoji/components/add_emoji.jsx @@ -0,0 +1,307 @@ +// 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 EmojiStore from 'stores/emoji_store.jsx'; + +import BackstageHeader from 'components/backstage/components/backstage_header.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 AddEmoji extends React.Component { + static propTypes = { + team: React.PropTypes.object.isRequired, + user: React.PropTypes.object.isRequired + } + + static contextTypes = { + router: React.PropTypes.object.isRequired + } + + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + + this.updateName = this.updateName.bind(this); + this.updateImage = this.updateImage.bind(this); + + this.state = { + name: '', + image: null, + imageUrl: '', + saving: false, + error: null + }; + } + + handleSubmit(e) { + e.preventDefault(); + + if (this.state.saving) { + return; + } + + this.setState({ + saving: true, + error: null + }); + + const emoji = { + creator_id: this.props.user.id, + name: this.state.name.trim().toLowerCase() + }; + + if (!emoji.name) { + this.setState({ + saving: false, + error: ( + <FormattedMessage + id='add_emoji.nameRequired' + defaultMessage='A name is required for the emoji' + /> + ) + }); + + return; + } else if (/[^a-z0-9_-]/.test(emoji.name)) { + this.setState({ + saving: false, + error: ( + <FormattedMessage + id='add_emoji.nameInvalid' + defaultMessage="An emoji's name can only contain lowercase letters, numbers, and the symbols '-' and '_'." + /> + ) + }); + + return; + } else if (EmojiStore.getSystemEmojis().has(emoji.name)) { + this.setState({ + saving: false, + error: ( + <FormattedMessage + id='add_emoji.nameTaken' + defaultMessage='This name is already in use by a system emoji. Please choose another name.' + /> + ) + }); + + return; + } + + if (!this.state.image) { + this.setState({ + saving: false, + error: ( + <FormattedMessage + id='add_emoji.imageRequired' + defaultMessage='An image is required for the emoji' + /> + ) + }); + + return; + } + + AsyncClient.addEmoji( + emoji, + this.state.image, + () => { + // for some reason, browserHistory.push doesn't trigger a state change even though the url changes + this.context.router.push('/' + this.props.team.name + '/emoji'); + }, + (err) => { + this.setState({ + saving: false, + error: err.message + }); + } + ); + } + + updateName(e) { + this.setState({ + name: e.target.value + }); + } + + updateImage(e) { + if (e.target.files.length === 0) { + this.setState({ + image: null, + imageUrl: '' + }); + + return; + } + + const image = e.target.files[0]; + + const reader = new FileReader(); + reader.onload = () => { + this.setState({ + image, + imageUrl: reader.result + }); + }; + reader.readAsDataURL(image); + } + + render() { + let filename = null; + if (this.state.image) { + filename = ( + <span className='add-emoji__filename'> + {this.state.image.name} + </span> + ); + } + + let preview = null; + if (this.state.imageUrl) { + preview = ( + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='preview' + > + <FormattedMessage + id='add_emoji.preview' + defaultMessage='Preview' + /> + </label> + <div className='col-md-5 col-sm-8 add-emoji__preview'> + <FormattedMessage + id='add_emoji.preview.sentence' + defaultMessage='This is a sentence with {image} in it.' + values={{ + image: ( + <img + className='emoticon' + src={this.state.imageUrl} + /> + ) + }} + /> + </div> + </div> + ); + } + + return ( + <div className='backstage-content row'> + <BackstageHeader> + <Link to={'/' + this.props.team.name + '/emoji'}> + <FormattedMessage + id='emoji_list.header' + defaultMessage='Custom Emoji' + /> + </Link> + <FormattedMessage + id='add_emoji.header' + defaultMessage='Add' + /> + </BackstageHeader> + <div className='backstage-form'> + <form + className='form-horizontal' + onSubmit={this.handleSubmit} + > + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='name' + > + <FormattedMessage + id='add_emoji.name' + defaultMessage='Name' + /> + </label> + <div className='col-md-5 col-sm-8'> + <input + id='name' + type='text' + maxLength='64' + className='form-control' + value={this.state.name} + onChange={this.updateName} + /> + <div className='form__help'> + <FormattedMessage + id='add_emoji.name.help' + defaultMessage="Choose a name for your emoji made of up to 64 characters consisting of lowercase letters, numbers, and the symbols '-' and '_'." + /> + </div> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='image' + > + <FormattedMessage + id='add_emoji.image' + defaultMessage='Image' + /> + </label> + <div className='col-md-5 col-sm-8'> + <div> + <div className='add-emoji__upload'> + <button className='btn btn-primary'> + <FormattedMessage + id='add_emoji.image.button' + defaultMessage='Select' + /> + </button> + <input + type='file' + accept='.jpg,.png,.gif' + multiple={false} + onChange={this.updateImage} + /> + </div> + {filename} + <div className='form__help'> + <FormattedMessage + id='add_emoji.image.help' + defaultMessage='Choose the image for your emoji. The image can be a gif, png, or jpeg file with a max size of 64 KB and dimensions up to 128 by 128 pixels.' + /> + </div> + </div> + </div> + </div> + {preview} + <div className='backstage-form__footer'> + <FormError error={this.state.error}/> + <Link + className='btn btn-sm' + to={'/' + this.props.team.name + '/emoji'} + > + <FormattedMessage + id='add_emoji.cancel' + defaultMessage='Cancel' + /> + </Link> + <SpinnerButton + className='btn btn-primary' + type='submit' + spinning={this.state.saving} + onClick={this.handleSubmit} + > + <FormattedMessage + id='add_emoji.save' + defaultMessage='Save' + /> + </SpinnerButton> + </div> + </form> + </div> + </div> + ); + } +} diff --git a/webapp/components/emoji/components/emoji_list.jsx b/webapp/components/emoji/components/emoji_list.jsx new file mode 100644 index 000000000..5795a57b2 --- /dev/null +++ b/webapp/components/emoji/components/emoji_list.jsx @@ -0,0 +1,218 @@ +// 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 EmojiStore from 'stores/emoji_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import {FormattedMessage} from 'react-intl'; +import EmojiListItem from './emoji_list_item.jsx'; +import {Link} from 'react-router'; +import LoadingScreen from 'components/loading_screen.jsx'; + +export default class EmojiList extends React.Component { + static get propTypes() { + return { + team: React.propTypes.object.isRequired, + user: React.propTypes.object.isRequired + }; + } + + constructor(props) { + super(props); + + this.canCreateEmojis = this.canCreateEmojis.bind(this); + + this.handleEmojiChange = this.handleEmojiChange.bind(this); + + this.deleteEmoji = this.deleteEmoji.bind(this); + + this.updateFilter = this.updateFilter.bind(this); + + this.state = { + emojis: EmojiStore.getCustomEmojiMap(), + loading: !EmojiStore.hasReceivedCustomEmojis(), + filter: '' + }; + } + + componentDidMount() { + EmojiStore.addChangeListener(this.handleEmojiChange); + + if (window.mm_config.EnableCustomEmoji === 'true') { + AsyncClient.listEmoji(); + } + } + + componentWillUnmount() { + EmojiStore.removeChangeListener(this.handleEmojiChange); + } + + handleEmojiChange() { + this.setState({ + emojis: EmojiStore.getCustomEmojiMap(), + loading: !EmojiStore.hasReceivedCustomEmojis() + }); + } + + updateFilter(e) { + this.setState({ + filter: e.target.value + }); + } + + deleteEmoji(emoji) { + AsyncClient.deleteEmoji(emoji.id); + } + + canCreateEmojis() { + if (global.window.mm_license.IsLicensed !== 'true') { + return true; + } + + if (Utils.isSystemAdmin(this.props.user.roles)) { + return true; + } + + if (window.mm_config.RestrictCustomEmojiCreation === 'all') { + return true; + } + + if (window.mm_config.RestrictCustomEmojiCreation === 'admin') { + // check whether the user is an admin on any of their teams + for (const member of TeamStore.getTeamMembers()) { + if (Utils.isAdmin(member.roles)) { + return true; + } + } + } + + return false; + } + + render() { + const filter = this.state.filter.toLowerCase(); + const isSystemAdmin = Utils.isSystemAdmin(this.props.user.roles); + + let emojis = []; + if (this.state.loading) { + emojis.push( + <LoadingScreen key='loading'/> + ); + } else if (this.state.emojis.length === 0) { + emojis.push( + <tr className='backstage-list__item backstage-list__empty'> + <td colSpan='4'> + <FormattedMessage + id='emoji_list.empty' + defaultMessage='No custom emoji found' + /> + </td> + </tr> + ); + } else { + for (const [, emoji] of this.state.emojis) { + let onDelete = null; + if (isSystemAdmin || this.props.user.id === emoji.creator_id) { + onDelete = this.deleteEmoji; + } + + emojis.push( + <EmojiListItem + key={emoji.id} + emoji={emoji} + onDelete={onDelete} + filter={filter} + /> + ); + } + } + + let addLink = null; + if (this.canCreateEmojis()) { + addLink = ( + <Link + className='add-link' + to={'/' + this.props.team.name + '/emoji/add'} + > + <button + type='button' + className='btn btn-primary' + > + <FormattedMessage + id='emoji_list.add' + defaultMessage='Add Custom Emoji' + /> + </button> + </Link> + ); + } + + return ( + <div className='backstage-content emoji-list'> + <div className='backstage-header'> + <h1> + <FormattedMessage + id='emoji_list.header' + defaultMessage='Custom Emoji' + /> + </h1> + {addLink} + </div> + <div className='backstage-filters'> + <div className='backstage-filter__search'> + <i className='fa fa-search'></i> + <input + type='search' + className='form-control' + placeholder={Utils.localizeMessage('emoji_list.search', 'Search Custom Emoji')} + value={this.state.filter} + onChange={this.updateFilter} + style={{flexGrow: 0, flexShrink: 0}} + /> + </div> + </div> + <span className='emoji-list__help'> + <FormattedMessage + id='emoji_list.help' + defaultMessage='Custom emoji are available to everyone on your server and will show up in the emoji autocomplete menu.' + /> + </span> + <div className='backstage-list'> + <table className='emoji-list__table'> + <tr className='backstage-list__item emoji-list__table-header'> + <th className='emoji-list__name'> + <FormattedMessage + id='emoji_list.name' + defaultMessage='Name' + /> + </th> + <th className='emoji-list__image'> + <FormattedMessage + id='emoji_list.image' + defaultMessage='Image' + /> + </th> + <th className='emoji-list__creator'> + <FormattedMessage + id='emoji_list.creator' + defaultMessage='Creator' + /> + </th> + <th className='emoji-list_actions'> + <FormattedMessage + id='emoji_list.actions' + defaultMessage='Actions' + /> + </th> + </tr> + {emojis} + </table> + </div> + </div> + ); + } +} diff --git a/webapp/components/emoji/components/emoji_list_item.jsx b/webapp/components/emoji/components/emoji_list_item.jsx new file mode 100644 index 000000000..50a4bacb1 --- /dev/null +++ b/webapp/components/emoji/components/emoji_list_item.jsx @@ -0,0 +1,118 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import EmojiStore from 'stores/emoji_store.jsx'; +import UserStore from 'stores/user_store.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import {FormattedMessage} from 'react-intl'; + +export default class EmojiListItem extends React.Component { + static get propTypes() { + return { + emoji: React.PropTypes.object.isRequired, + onDelete: React.PropTypes.func.isRequired, + filter: React.PropTypes.string + }; + } + + constructor(props) { + super(props); + + this.handleDelete = this.handleDelete.bind(this); + + this.state = { + creator: UserStore.getProfile(this.props.emoji.creator_id) + }; + } + + handleDelete(e) { + e.preventDefault(); + + this.props.onDelete(this.props.emoji); + } + + matchesFilter(emoji, creator, filter) { + if (!filter) { + return true; + } + + if (emoji.name.toLowerCase().indexOf(filter) !== -1) { + return true; + } + + if (creator) { + if (creator.username.toLowerCase().indexOf(filter) !== -1 || + (creator.first_name && creator.first_name.toLowerCase().indexOf(filter)) || + (creator.last_name && creator.last_name.toLowerCase().indexOf(filter)) || + (creator.nickname && creator.nickname.toLowerCase().indexOf(filter))) { + return true; + } + } + + return false; + } + + render() { + const emoji = this.props.emoji; + const creator = this.state.creator; + const filter = this.props.filter ? this.props.filter.toLowerCase() : ''; + + if (!this.matchesFilter(emoji, creator, filter)) { + return null; + } + + let creatorName; + if (creator) { + creatorName = Utils.displayUsernameForUser(creator); + + if (creatorName !== creator.username) { + creatorName += ' (@' + creator.username + ')'; + } + } else { + creatorName = ( + <FormattedMessage + id='emoji_list.somebody' + defaultMessage='Somebody on another team' + /> + ); + } + + let deleteButton = null; + if (this.props.onDelete) { + deleteButton = ( + <a + href='#' + onClick={this.handleDelete} + > + <FormattedMessage + id='emoji_list.delete' + defaultMessage='Delete' + /> + </a> + ); + } + + return ( + <tr className='backstage-list__item'> + <td className='emoji-list__name'> + {':' + emoji.name + ':'} + </td> + <td className='emoji-list__image'> + <img + className='emoticon' + src={EmojiStore.getEmojiImageUrl(emoji)} + /> + </td> + <td className='emoji-list__creator'> + {creatorName} + </td> + <td className='emoji-list-item_actions'> + {deleteButton} + </td> + </tr> + ); + } +} diff --git a/webapp/components/backstage/add_command.jsx b/webapp/components/integrations/components/add_command.jsx index 91af0416b..e72670e47 100644 --- a/webapp/components/backstage/add_command.jsx +++ b/webapp/components/integrations/components/add_command.jsx @@ -6,7 +6,7 @@ import React from 'react'; import * as AsyncClient from 'utils/async_client.jsx'; import * as Utils from 'utils/utils.jsx'; -import BackstageHeader from './backstage_header.jsx'; +import BackstageHeader from 'components/backstage/components/backstage_header.jsx'; import {FormattedMessage} from 'react-intl'; import FormError from 'components/form_error.jsx'; import {browserHistory, Link} from 'react-router/es6'; @@ -17,6 +17,12 @@ const REQUEST_POST = 'P'; const REQUEST_GET = 'G'; export default class AddCommand extends React.Component { + static get propTypes() { + return { + team: React.propTypes.object.isRequired + }; + } + constructor(props) { super(props); @@ -155,7 +161,7 @@ export default class AddCommand extends React.Component { AsyncClient.addCommand( command, () => { - browserHistory.push('/' + Utils.getTeamNameFromUrl() + '/settings/integrations/commands'); + browserHistory.push('/' + this.props.team.name + '/integrations/commands'); }, (err) => { this.setState({ @@ -300,7 +306,7 @@ export default class AddCommand extends React.Component { return ( <div className='backstage-content row'> <BackstageHeader> - <Link to={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/commands'}> + <Link to={'/' + this.props.team.name + '/integrations/commands'}> <FormattedMessage id='installed_command.header' defaultMessage='Slash Commands' @@ -312,7 +318,10 @@ export default class AddCommand extends React.Component { /> </BackstageHeader> <div className='backstage-form'> - <form className='form-horizontal'> + <form + className='form-horizontal' + onSubmit={this.handleSubmit} + > <div className='form-group'> <label className='control-label col-sm-4' @@ -531,7 +540,7 @@ export default class AddCommand extends React.Component { <FormError errors={[this.state.serverError, this.state.clientError]}/> <Link className='btn btn-sm' - to={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/commands'} + to={'/' + this.props.team.name + '/integrations/commands'} > <FormattedMessage id='add_command.cancel' diff --git a/webapp/components/backstage/add_incoming_webhook.jsx b/webapp/components/integrations/components/add_incoming_webhook.jsx index 528f03377..122600c90 100644 --- a/webapp/components/backstage/add_incoming_webhook.jsx +++ b/webapp/components/integrations/components/add_incoming_webhook.jsx @@ -4,9 +4,8 @@ import React from 'react'; import * as AsyncClient from 'utils/async_client.jsx'; -import * as Utils from 'utils/utils.jsx'; -import BackstageHeader from './backstage_header.jsx'; +import BackstageHeader from 'components/backstage/components/backstage_header.jsx'; import ChannelSelect from 'components/channel_select.jsx'; import {FormattedMessage} from 'react-intl'; import FormError from 'components/form_error.jsx'; @@ -14,6 +13,12 @@ import {browserHistory, Link} from 'react-router/es6'; import SpinnerButton from 'components/spinner_button.jsx'; export default class AddIncomingWebhook extends React.Component { + static get propTypes() { + return { + team: React.propTypes.object.isRequired + }; + } + constructor(props) { super(props); @@ -69,7 +74,7 @@ export default class AddIncomingWebhook extends React.Component { AsyncClient.addIncomingHook( hook, () => { - browserHistory.push('/' + Utils.getTeamNameFromUrl() + '/settings/integrations/incoming_webhooks'); + browserHistory.push('/' + this.props.team.name + '/integrations/incoming_webhooks'); }, (err) => { this.setState({ @@ -102,7 +107,7 @@ export default class AddIncomingWebhook extends React.Component { return ( <div className='backstage-content'> <BackstageHeader> - <Link to={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/incoming_webhooks'}> + <Link to={'/' + this.props.team.name + '/integrations/incoming_webhooks'}> <FormattedMessage id='installed_incoming_webhooks.header' defaultMessage='Incoming Webhooks' @@ -114,7 +119,10 @@ export default class AddIncomingWebhook extends React.Component { /> </BackstageHeader> <div className='backstage-form'> - <form className='form-horizontal'> + <form + className='form-horizontal' + onSubmit={this.handleSubmit} + > <div className='form-group'> <label className='control-label col-sm-4' @@ -181,7 +189,7 @@ export default class AddIncomingWebhook extends React.Component { <FormError errors={[this.state.serverError, this.state.clientError]}/> <Link className='btn btn-sm' - to={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/incoming_webhooks'} + to={'/' + this.props.team.name + '/integrations/incoming_webhooks'} > <FormattedMessage id='add_incoming_webhook.cancel' diff --git a/webapp/components/backstage/add_outgoing_webhook.jsx b/webapp/components/integrations/components/add_outgoing_webhook.jsx index 5f9d96249..bd49fedc9 100644 --- a/webapp/components/backstage/add_outgoing_webhook.jsx +++ b/webapp/components/integrations/components/add_outgoing_webhook.jsx @@ -4,9 +4,8 @@ import React from 'react'; import * as AsyncClient from 'utils/async_client.jsx'; -import * as Utils from 'utils/utils.jsx'; -import BackstageHeader from './backstage_header.jsx'; +import BackstageHeader from 'components/backstage/components/backstage_header.jsx'; import ChannelSelect from 'components/channel_select.jsx'; import {FormattedMessage} from 'react-intl'; import FormError from 'components/form_error.jsx'; @@ -14,6 +13,12 @@ import {browserHistory, Link} from 'react-router/es6'; import SpinnerButton from 'components/spinner_button.jsx'; export default class AddOutgoingWebhook extends React.Component { + static get propTypes() { + return { + team: React.propTypes.object.isRequired + }; + } + constructor(props) { super(props); @@ -112,7 +117,7 @@ export default class AddOutgoingWebhook extends React.Component { AsyncClient.addOutgoingHook( hook, () => { - browserHistory.push('/' + Utils.getTeamNameFromUrl() + '/settings/integrations/outgoing_webhooks'); + browserHistory.push('/' + this.props.team.name + '/integrations/outgoing_webhooks'); }, (err) => { this.setState({ @@ -165,7 +170,7 @@ export default class AddOutgoingWebhook extends React.Component { return ( <div className='backstage-content'> <BackstageHeader> - <Link to={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/outgoing_webhooks'}> + <Link to={'/' + this.props.team.name + '/integrations/outgoing_webhooks'}> <FormattedMessage id='installed_outgoing_webhooks.header' defaultMessage='Outgoing Webhooks' @@ -177,7 +182,10 @@ export default class AddOutgoingWebhook extends React.Component { /> </BackstageHeader> <div className='backstage-form'> - <form className='form-horizontal'> + <form + className='form-horizontal' + onSubmit={this.handleSubmit} + > <div className='form-group'> <label className='control-label col-sm-4' @@ -314,7 +322,7 @@ export default class AddOutgoingWebhook extends React.Component { <FormError errors={[this.state.serverError, this.state.clientError]}/> <Link className='btn btn-sm' - to={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/outgoing_webhooks'} + to={'/' + this.props.team.name + '/integrations/outgoing_webhooks'} > <FormattedMessage id='add_outgoing_webhook.cancel' diff --git a/webapp/components/backstage/installed_command.jsx b/webapp/components/integrations/components/installed_command.jsx index 88f43f674..658126f19 100644 --- a/webapp/components/backstage/installed_command.jsx +++ b/webapp/components/integrations/components/installed_command.jsx @@ -50,8 +50,9 @@ export default class InstalledCommand extends React.Component { render() { const command = this.props.command; + const filter = this.props.filter ? this.props.filter.toLowerCase() : ''; - if (!this.matchesFilter(command, this.props.filter)) { + if (!this.matchesFilter(command, filter)) { return null; } @@ -61,7 +62,7 @@ export default class InstalledCommand extends React.Component { } else { name = ( <FormattedMessage - id='installed_integraions.unnamed_command' + id='installed_commands.unnamed_command' defaultMessage='Unnamed Slash Command' /> ); diff --git a/webapp/components/backstage/installed_commands.jsx b/webapp/components/integrations/components/installed_commands.jsx index df1f56687..597ba7005 100644 --- a/webapp/components/backstage/installed_commands.jsx +++ b/webapp/components/integrations/components/installed_commands.jsx @@ -8,11 +8,17 @@ import IntegrationStore from 'stores/integration_store.jsx'; import TeamStore from 'stores/team_store.jsx'; import * as Utils from 'utils/utils.jsx'; +import BackstageList from 'components/backstage/components/backstage_list.jsx'; import {FormattedMessage} from 'react-intl'; import InstalledCommand from './installed_command.jsx'; -import InstalledIntegrations from './installed_integrations.jsx'; export default class InstalledCommands extends React.Component { + static get propTypes() { + return { + team: React.propTypes.object.isRequired + }; + } + constructor(props) { super(props); @@ -71,7 +77,7 @@ export default class InstalledCommands extends React.Component { }); return ( - <InstalledIntegrations + <BackstageList header={ <FormattedMessage id='installed_commands.header' @@ -84,17 +90,18 @@ export default class InstalledCommands extends React.Component { defaultMessage='Add Slash Command' /> } - addLink={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/commands/add'} + addLink={'/' + this.props.team.name + '/integrations/commands/add'} emptyText={ <FormattedMessage id='installed_commands.empty' defaultMessage='No slash commands found' /> } + searchPlaceholder={Utils.localizeMessage('installed_commands.search', 'Search Slash Commands')} loading={this.state.loading} > {commands} - </InstalledIntegrations> + </BackstageList> ); } } diff --git a/webapp/components/backstage/installed_incoming_webhook.jsx b/webapp/components/integrations/components/installed_incoming_webhook.jsx index afa6e9958..2cf3f24b8 100644 --- a/webapp/components/backstage/installed_incoming_webhook.jsx +++ b/webapp/components/integrations/components/installed_incoming_webhook.jsx @@ -51,8 +51,9 @@ export default class InstalledIncomingWebhook extends React.Component { render() { const incomingWebhook = this.props.incomingWebhook; const channel = ChannelStore.get(incomingWebhook.channel_id); + const filter = this.props.filter ? this.props.filter.toLowerCase() : ''; - if (!this.matchesFilter(incomingWebhook, channel, this.props.filter)) { + if (!this.matchesFilter(incomingWebhook, channel, filter)) { return null; } diff --git a/webapp/components/backstage/installed_incoming_webhooks.jsx b/webapp/components/integrations/components/installed_incoming_webhooks.jsx index 0a38a6ab5..a3bcf904e 100644 --- a/webapp/components/backstage/installed_incoming_webhooks.jsx +++ b/webapp/components/integrations/components/installed_incoming_webhooks.jsx @@ -8,11 +8,17 @@ import IntegrationStore from 'stores/integration_store.jsx'; import TeamStore from 'stores/team_store.jsx'; import * as Utils from 'utils/utils.jsx'; +import BackstageList from 'components/backstage/components/backstage_list.jsx'; import {FormattedMessage} from 'react-intl'; import InstalledIncomingWebhook from './installed_incoming_webhook.jsx'; -import InstalledIntegrations from './installed_integrations.jsx'; export default class InstalledIncomingWebhooks extends React.Component { + static get propTypes() { + return { + team: React.propTypes.object.isRequired + }; + } + constructor(props) { super(props); @@ -65,7 +71,7 @@ export default class InstalledIncomingWebhooks extends React.Component { }); return ( - <InstalledIntegrations + <BackstageList header={ <FormattedMessage id='installed_incoming_webhooks.header' @@ -78,17 +84,18 @@ export default class InstalledIncomingWebhooks extends React.Component { defaultMessage='Add Incoming Webhook' /> } - addLink={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/incoming_webhooks/add'} + addLink={'/' + this.props.team.name + '/integrations/incoming_webhooks/add'} emptyText={ <FormattedMessage id='installed_incoming_webhooks.empty' defaultMessage='No incoming webhooks found' /> } + searchPlaceholder={Utils.localizeMessage('installed_incoming_webhooks.search', 'Search Incoming Webhooks')} loading={this.state.loading} > {incomingWebhooks} - </InstalledIntegrations> + </BackstageList> ); } } diff --git a/webapp/components/backstage/installed_outgoing_webhook.jsx b/webapp/components/integrations/components/installed_outgoing_webhook.jsx index 99f2439ec..852231823 100644 --- a/webapp/components/backstage/installed_outgoing_webhook.jsx +++ b/webapp/components/integrations/components/installed_outgoing_webhook.jsx @@ -65,8 +65,9 @@ export default class InstalledOutgoingWebhook extends React.Component { render() { const outgoingWebhook = this.props.outgoingWebhook; const channel = ChannelStore.get(outgoingWebhook.channel_id); + const filter = this.props.filter ? this.props.filter.toLowerCase() : ''; - if (!this.matchesFilter(outgoingWebhook, channel, this.props.filter)) { + if (!this.matchesFilter(outgoingWebhook, channel, filter)) { return null; } diff --git a/webapp/components/backstage/installed_outgoing_webhooks.jsx b/webapp/components/integrations/components/installed_outgoing_webhooks.jsx index b79bc3530..ebc9a6fc1 100644 --- a/webapp/components/backstage/installed_outgoing_webhooks.jsx +++ b/webapp/components/integrations/components/installed_outgoing_webhooks.jsx @@ -8,11 +8,17 @@ import IntegrationStore from 'stores/integration_store.jsx'; import TeamStore from 'stores/team_store.jsx'; import * as Utils from 'utils/utils.jsx'; +import BackstageList from 'components/backstage/components/backstage_list.jsx'; import {FormattedMessage} from 'react-intl'; import InstalledOutgoingWebhook from './installed_outgoing_webhook.jsx'; -import InstalledIntegrations from './installed_integrations.jsx'; export default class InstalledOutgoingWebhooks extends React.Component { + static get propTypes() { + return { + team: React.propTypes.object.isRequired + }; + } + constructor(props) { super(props); @@ -71,7 +77,7 @@ export default class InstalledOutgoingWebhooks extends React.Component { }); return ( - <InstalledIntegrations + <BackstageList header={ <FormattedMessage id='installed_outgoing_webhooks.header' @@ -84,17 +90,18 @@ export default class InstalledOutgoingWebhooks extends React.Component { defaultMessage='Add Outgoing Webhook' /> } - addLink={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/outgoing_webhooks/add'} + addLink={'/' + this.props.team.name + '/integrations/outgoing_webhooks/add'} emptyText={ <FormattedMessage id='installed_outgoing_webhooks.empty' defaultMessage='No outgoing webhooks found' /> } + searchPlaceholder={Utils.localizeMessage('installed_outgoing_webhooks.search', 'Search Outgoing Webhooks')} loading={this.state.loading} > {outgoingWebhooks} - </InstalledIntegrations> + </BackstageList> ); } } diff --git a/webapp/components/backstage/integration_option.jsx b/webapp/components/integrations/components/integration_option.jsx index 483e6a888..483e6a888 100644 --- a/webapp/components/backstage/integration_option.jsx +++ b/webapp/components/integrations/components/integration_option.jsx diff --git a/webapp/components/backstage/integrations.jsx b/webapp/components/integrations/components/integrations.jsx index fdd75026a..7894ced5d 100644 --- a/webapp/components/backstage/integrations.jsx +++ b/webapp/components/integrations/components/integrations.jsx @@ -5,11 +5,16 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; import IntegrationOption from './integration_option.jsx'; -import * as Utils from 'utils/utils.jsx'; import WebhookIcon from 'images/webhook_icon.jpg'; export default class Integrations extends React.Component { + static get propTypes() { + return { + team: React.propTypes.object.isRequired + }; + } + render() { const options = []; @@ -30,7 +35,7 @@ export default class Integrations extends React.Component { defaultMessage='Incoming webhooks allow external integrations to send messages' /> } - link={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/incoming_webhooks'} + link={'/' + this.props.team.name + '/integrations/incoming_webhooks'} /> ); } @@ -52,7 +57,7 @@ export default class Integrations extends React.Component { defaultMessage='Outgoing webhooks allow external integrations to receive and respond to messages' /> } - link={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/outgoing_webhooks'} + link={'/' + this.props.team.name + '/integrations/outgoing_webhooks'} /> ); } @@ -74,7 +79,7 @@ export default class Integrations extends React.Component { defaultMessage='Slash commands send events to an external integration' /> } - link={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/commands'} + link={'/' + this.props.team.name + '/integrations/commands'} /> ); } diff --git a/webapp/components/logged_in.jsx b/webapp/components/logged_in.jsx index 6752b56cd..484164e56 100644 --- a/webapp/components/logged_in.jsx +++ b/webapp/components/logged_in.jsx @@ -156,6 +156,11 @@ export default class LoggedIn extends React.Component { e.preventDefault(); } }); + + // Get custom emoji from the server + if (window.mm_config.EnableCustomEmoji === 'true') { + AsyncClient.listEmoji(); + } } componentWillUnmount() { @@ -187,4 +192,4 @@ export default class LoggedIn extends React.Component { LoggedIn.propTypes = { children: React.PropTypes.object -};
\ No newline at end of file +}; diff --git a/webapp/components/navbar_dropdown.jsx b/webapp/components/navbar_dropdown.jsx index c3b646e52..4f137979e 100644 --- a/webapp/components/navbar_dropdown.jsx +++ b/webapp/components/navbar_dropdown.jsx @@ -85,6 +85,7 @@ export default class NavbarDropdown extends React.Component { var isSystemAdmin = false; var teamSettings = null; let integrationsLink = null; + let customEmojiLink = null; if (currentUser != null) { isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser(); @@ -166,7 +167,7 @@ export default class NavbarDropdown extends React.Component { if (integrationsEnabled && (isAdmin || window.mm_config.EnableOnlyAdminIntegrations !== 'true')) { integrationsLink = ( <li> - <Link to={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations'}> + <Link to={'/' + Utils.getTeamNameFromUrl() + '/integrations'}> <FormattedMessage id='navbar_dropdown.integrations' defaultMessage='Integrations' @@ -176,6 +177,19 @@ export default class NavbarDropdown extends React.Component { ); } + if (window.mm_config.EnableCustomEmoji === 'true') { + customEmojiLink = ( + <li> + <Link to={'/' + Utils.getTeamNameFromUrl() + '/emoji'}> + <FormattedMessage + id='navbar_dropdown.emoji' + defaultMessage='Custom Emoji' + /> + </Link> + </li> + ); + } + if (isSystemAdmin) { sysAdminLink = ( <li> @@ -327,8 +341,10 @@ export default class NavbarDropdown extends React.Component { </a> </li> <li className='divider'></li> - {teamSettings} {integrationsLink} + {customEmojiLink} + <li className='divider'></li> + {teamSettings} {manageLink} {sysAdminLink} {teams} diff --git a/webapp/components/post_view/components/post.jsx b/webapp/components/post_view/components/post.jsx index c445ad9f3..6633bd9b9 100644 --- a/webapp/components/post_view/components/post.jsx +++ b/webapp/components/post_view/components/post.jsx @@ -84,6 +84,10 @@ export default class Post extends React.Component { return true; } + if (nextProps.emojis !== this.props.emojis) { + return true; + } + return false; } render() { @@ -200,6 +204,7 @@ export default class Post extends React.Component { handleCommentClick={this.handleCommentClick} compactDisplay={this.props.compactDisplay} previewCollapsed={this.props.previewCollapsed} + emojis={this.props.emojis} /> </div> </div> @@ -225,5 +230,6 @@ Post.propTypes = { compactDisplay: React.PropTypes.bool, previewCollapsed: React.PropTypes.string, commentCount: React.PropTypes.number, - useMilitaryTime: React.PropTypes.bool.isRequired + useMilitaryTime: React.PropTypes.bool.isRequired, + emojis: React.PropTypes.object.isRequired }; diff --git a/webapp/components/post_view/components/post_body.jsx b/webapp/components/post_view/components/post_body.jsx index 2a2be75a9..561860b65 100644 --- a/webapp/components/post_view/components/post_body.jsx +++ b/webapp/components/post_view/components/post_body.jsx @@ -37,6 +37,10 @@ export default class PostBody extends React.Component { return true; } + if (nextProps.emojis !== this.props.emojis) { + return true; + } + return false; } @@ -151,7 +155,7 @@ export default class PostBody extends React.Component { message = ( <span onClick={TextFormatting.handleClick} - dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message)}} + dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message, {emojis: this.props.emojis})}} /> ); } @@ -199,5 +203,6 @@ PostBody.propTypes = { retryPost: React.PropTypes.func.isRequired, handleCommentClick: React.PropTypes.func.isRequired, compactDisplay: React.PropTypes.bool, - previewCollapsed: React.PropTypes.string + previewCollapsed: React.PropTypes.string, + emojis: React.PropTypes.object.isRequired }; diff --git a/webapp/components/post_view/components/post_body_additional_content.jsx b/webapp/components/post_view/components/post_body_additional_content.jsx index 0ab015ced..a51a557b9 100644 --- a/webapp/components/post_view/components/post_body_additional_content.jsx +++ b/webapp/components/post_view/components/post_body_additional_content.jsx @@ -35,6 +35,9 @@ export default class PostBodyAdditionalContent extends React.Component { if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) { return true; } + if (!Utils.areObjectsEqual(nextProps.message, this.props.message)) { + return true; + } if (nextState.embedVisible !== this.state.embedVisible) { return true; } diff --git a/webapp/components/post_view/components/post_list.jsx b/webapp/components/post_view/components/post_list.jsx index 288a2d5e0..bcd763d58 100644 --- a/webapp/components/post_view/components/post_list.jsx +++ b/webapp/components/post_view/components/post_list.jsx @@ -265,6 +265,7 @@ export default class PostList extends React.Component { compactDisplay={this.props.compactDisplay} previewCollapsed={this.props.previewsCollapsed} useMilitaryTime={this.props.useMilitaryTime} + emojis={this.props.emojis} /> ); @@ -527,5 +528,6 @@ PostList.propTypes = { compactDisplay: React.PropTypes.bool, previewsCollapsed: React.PropTypes.string, useMilitaryTime: React.PropTypes.bool.isRequired, - isFocusPost: React.PropTypes.bool + isFocusPost: React.PropTypes.bool, + emojis: React.PropTypes.object.isRequired }; diff --git a/webapp/components/post_view/post_focus_view_controller.jsx b/webapp/components/post_view/post_focus_view_controller.jsx index c70ebb0f5..f8738e056 100644 --- a/webapp/components/post_view/post_focus_view_controller.jsx +++ b/webapp/components/post_view/post_focus_view_controller.jsx @@ -4,6 +4,7 @@ import PostList from './components/post_list.jsx'; import LoadingScreen from 'components/loading_screen.jsx'; +import EmojiStore from 'stores/emoji_store.jsx'; import PostStore from 'stores/post_store.jsx'; import UserStore from 'stores/user_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; @@ -20,6 +21,7 @@ export default class PostFocusView extends React.Component { this.onChannelChange = this.onChannelChange.bind(this); this.onPostsChange = this.onPostsChange.bind(this); this.onUserChange = this.onUserChange.bind(this); + this.onEmojiChange = this.onEmojiChange.bind(this); this.onPostListScroll = this.onPostListScroll.bind(this); const focusedPostId = PostStore.getFocusedPostId(); @@ -38,7 +40,8 @@ export default class PostFocusView extends React.Component { currentChannel: ChannelStore.getCurrentId().slice(), scrollPostId: focusedPostId, atTop: PostStore.getVisibilityAtTop(focusedPostId), - atBottom: PostStore.getVisibilityAtBottom(focusedPostId) + atBottom: PostStore.getVisibilityAtBottom(focusedPostId), + emojis: EmojiStore.getEmojis() }; } @@ -46,12 +49,14 @@ export default class PostFocusView extends React.Component { ChannelStore.addChangeListener(this.onChannelChange); PostStore.addChangeListener(this.onPostsChange); UserStore.addChangeListener(this.onUserChange); + EmojiStore.addChangeListener(this.onEmojiChange); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onChannelChange); PostStore.removeChangeListener(this.onPostsChange); UserStore.removeChangeListener(this.onUserChange); + EmojiStore.removeChangeListener(this.onEmojiChange); } onChannelChange() { @@ -87,6 +92,12 @@ export default class PostFocusView extends React.Component { this.setState({currentUser: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(profiles))}); } + onEmojiChange() { + this.setState({ + emojis: EmojiStore.getEmojis() + }); + } + onPostListScroll() { this.setState({scrollType: ScrollTypes.FREE}); } @@ -116,6 +127,7 @@ export default class PostFocusView extends React.Component { showMoreMessagesBottom={!this.state.atBottom} postsToHighlight={postsToHighlight} isFocusPost={true} + emojis={this.state.emojis} /> ); } diff --git a/webapp/components/post_view/post_view_controller.jsx b/webapp/components/post_view/post_view_controller.jsx index 6d724f659..17c3e94ae 100644 --- a/webapp/components/post_view/post_view_controller.jsx +++ b/webapp/components/post_view/post_view_controller.jsx @@ -4,6 +4,7 @@ import PostList from './components/post_list.jsx'; import LoadingScreen from 'components/loading_screen.jsx'; +import EmojiStore from 'stores/emoji_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import UserStore from 'stores/user_store.jsx'; import PostStore from 'stores/post_store.jsx'; @@ -24,6 +25,7 @@ export default class PostViewController extends React.Component { this.onPreferenceChange = this.onPreferenceChange.bind(this); this.onUserChange = this.onUserChange.bind(this); this.onPostsChange = this.onPostsChange.bind(this); + this.onEmojisChange = this.onEmojisChange.bind(this); this.onPostsViewJumpRequest = this.onPostsViewJumpRequest.bind(this); this.onPostListScroll = this.onPostListScroll.bind(this); this.onActivate = this.onActivate.bind(this); @@ -53,7 +55,8 @@ export default class PostViewController extends React.Component { displayPostsInCenter: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED, compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT, previewsCollapsed: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLLAPSE_DISPLAY, 'false'), - useMilitaryTime: PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false) + useMilitaryTime: PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false), + emojis: EmojiStore.getEmojis() }; } @@ -102,11 +105,18 @@ export default class PostViewController extends React.Component { }); } + onEmojisChange() { + this.setState({ + emojis: EmojiStore.getEmojis() + }); + } + onActivate() { PreferenceStore.addChangeListener(this.onPreferenceChange); UserStore.addChangeListener(this.onUserChange); PostStore.addChangeListener(this.onPostsChange); PostStore.addPostsViewJumpListener(this.onPostsViewJumpRequest); + EmojiStore.addChangeListener(this.onEmojisChange); } onDeactivate() { @@ -114,6 +124,7 @@ export default class PostViewController extends React.Component { UserStore.removeChangeListener(this.onUserChange); PostStore.removeChangeListener(this.onPostsChange); PostStore.removePostsViewJumpListener(this.onPostsViewJumpRequest); + EmojiStore.removeChangeListener(this.onEmojisChange); } componentWillReceiveProps(nextProps) { @@ -265,6 +276,7 @@ export default class PostViewController extends React.Component { previewsCollapsed={this.state.previewsCollapsed} useMilitaryTime={this.state.useMilitaryTime} lastViewed={this.state.lastViewed} + emojis={this.state.emojis} /> ); } diff --git a/webapp/components/root.jsx b/webapp/components/root.jsx index 977652e99..f98ed4c3d 100644 --- a/webapp/components/root.jsx +++ b/webapp/components/root.jsx @@ -1,9 +1,6 @@ // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -//import $ from 'jquery'; -//import Client from 'utils/web_client.jsx'; - import * as GlobalActions from 'actions/global_actions.jsx'; import LocalizationStore from 'stores/localization_store.jsx'; import Client from 'utils/web_client.jsx'; diff --git a/webapp/components/suggestion/emoticon_provider.jsx b/webapp/components/suggestion/emoticon_provider.jsx index 7a45c5b4e..af8cac070 100644 --- a/webapp/components/suggestion/emoticon_provider.jsx +++ b/webapp/components/suggestion/emoticon_provider.jsx @@ -3,6 +3,7 @@ import React from 'react'; +import EmojiStore from 'stores/emoji_store.jsx'; import * as Emoticons from 'utils/emoticons.jsx'; import SuggestionStore from 'stores/suggestion_store.jsx'; @@ -29,7 +30,7 @@ class EmoticonSuggestion extends Suggestion { <img alt={text} className='emoticon-suggestion__image' - src={emoticon.path} + src={EmojiStore.getEmojiImageUrl(emoticon)} title={text} /> </div> @@ -53,21 +54,19 @@ export default class EmoticonProvider { const matched = []; - const emoticons = Emoticons.getEmoticonsByName(); - // check for text emoticons for (const emoticon of Object.keys(Emoticons.emoticonPatterns)) { if (Emoticons.emoticonPatterns[emoticon].test(text)) { - SuggestionStore.addSuggestion(suggestionId, text, emoticons.get(emoticon), EmoticonSuggestion, text); + SuggestionStore.addSuggestion(suggestionId, text, EmojiStore.get(emoticon), EmoticonSuggestion, text); hasSuggestions = true; } } - // checked for named emoji - for (const [name, emoticon] of emoticons) { + // check for named emoji + for (const [name, emoji] of EmojiStore.getEmojis()) { if (name.indexOf(partialName) !== -1) { - matched.push(emoticon); + matched.push(emoji); if (matched.length >= MAX_EMOTICON_SUGGESTIONS) { break; @@ -77,11 +76,11 @@ export default class EmoticonProvider { // sort the emoticons so that emoticons starting with the entered text come first matched.sort((a, b) => { - const aPrefix = a.alias.startsWith(partialName); - const bPrefix = b.alias.startsWith(partialName); + const aPrefix = a.name.startsWith(partialName); + const bPrefix = b.name.startsWith(partialName); if (aPrefix === bPrefix) { - return a.alias.localeCompare(b.alias); + return a.name.localeCompare(b.name); } else if (aPrefix) { return -1; } @@ -89,7 +88,7 @@ export default class EmoticonProvider { return 1; }); - const terms = matched.map((emoticon) => ':' + emoticon.alias + ':'); + const terms = matched.map((emoticon) => ':' + emoticon.name + ':'); if (terms.length > 0) { SuggestionStore.addSuggestions(suggestionId, terms, matched, EmoticonSuggestion, text); |