summaryrefslogtreecommitdiffstats
path: root/webapp/components
diff options
context:
space:
mode:
authorHarrison Healey <harrisonmhealey@gmail.com>2016-07-05 11:58:18 -0400
committerJoram Wilander <jwawilander@gmail.com>2016-07-05 11:58:18 -0400
commitdc2f2a800105b77e665ec2a00c6290f35b1a2ba3 (patch)
tree82f23c2e72a7c785f55c2d6c1c35c10c16994918 /webapp/components
parenta65f1fc266f15eaa8f79541d4d11440c3d356bb6 (diff)
downloadchat-dc2f2a800105b77e665ec2a00c6290f35b1a2ba3.tar.gz
chat-dc2f2a800105b77e665ec2a00c6290f35b1a2ba3.tar.bz2
chat-dc2f2a800105b77e665ec2a00c6290f35b1a2ba3.zip
PLT-3145 Custom Emojis (#3381)
* Reorganized Backstage code to use a view controller and separated it from integrations code * Renamed InstalledIntegrations component to BackstageList * Added EmojiList page * Added AddEmoji page * Added custom emoji to autocomplete and text formatter * Moved system emoji to EmojiStore * Stopped trying to get emoji before logging in * Rerender posts when emojis change * Fixed submit handler on backstage pages to properly support enter * Removed debugging code * Updated javascript driver * Fixed unit tests * Fixed backstage routes * Added clientside validation to prevent users from creating an emoji with the same name as a system one * Fixed AddEmoji page to properly redirect when an emoji is created successfully * Fixed updating emoji list when an emoji is deleted * Added type prop to BackstageList to properly support using a table for the list * Added help text to EmojiList * Fixed backstage on smaller screen sizes * Disable custom emoji by default * Improved restrictions on creating emojis * Fixed non-admin users seeing the option to delete each other's emojis * Fixing gofmt * Fixed emoji unit tests * Fixed trying to get emoji from the server when it's disabled
Diffstat (limited to 'webapp/components')
-rw-r--r--webapp/components/admin_console/custom_emoji_settings.jsx51
-rw-r--r--webapp/components/backstage/backstage_controller.jsx71
-rw-r--r--webapp/components/backstage/components/backstage_category.jsx (renamed from webapp/components/backstage/backstage_category.jsx)1
-rw-r--r--webapp/components/backstage/components/backstage_header.jsx (renamed from webapp/components/backstage/backstage_header.jsx)0
-rw-r--r--webapp/components/backstage/components/backstage_list.jsx108
-rw-r--r--webapp/components/backstage/components/backstage_navbar.jsx (renamed from webapp/components/backstage/backstage_navbar.jsx)36
-rw-r--r--webapp/components/backstage/components/backstage_section.jsx (renamed from webapp/components/backstage/backstage_section.jsx)0
-rw-r--r--webapp/components/backstage/components/backstage_sidebar.jsx (renamed from webapp/components/backstage/backstage_sidebar.jsx)79
-rw-r--r--webapp/components/backstage/installed_integrations.jsx101
-rw-r--r--webapp/components/emoji/components/add_emoji.jsx307
-rw-r--r--webapp/components/emoji/components/emoji_list.jsx218
-rw-r--r--webapp/components/emoji/components/emoji_list_item.jsx118
-rw-r--r--webapp/components/integrations/components/add_command.jsx (renamed from webapp/components/backstage/add_command.jsx)19
-rw-r--r--webapp/components/integrations/components/add_incoming_webhook.jsx (renamed from webapp/components/backstage/add_incoming_webhook.jsx)20
-rw-r--r--webapp/components/integrations/components/add_outgoing_webhook.jsx (renamed from webapp/components/backstage/add_outgoing_webhook.jsx)20
-rw-r--r--webapp/components/integrations/components/installed_command.jsx (renamed from webapp/components/backstage/installed_command.jsx)5
-rw-r--r--webapp/components/integrations/components/installed_commands.jsx (renamed from webapp/components/backstage/installed_commands.jsx)15
-rw-r--r--webapp/components/integrations/components/installed_incoming_webhook.jsx (renamed from webapp/components/backstage/installed_incoming_webhook.jsx)3
-rw-r--r--webapp/components/integrations/components/installed_incoming_webhooks.jsx (renamed from webapp/components/backstage/installed_incoming_webhooks.jsx)15
-rw-r--r--webapp/components/integrations/components/installed_outgoing_webhook.jsx (renamed from webapp/components/backstage/installed_outgoing_webhook.jsx)3
-rw-r--r--webapp/components/integrations/components/installed_outgoing_webhooks.jsx (renamed from webapp/components/backstage/installed_outgoing_webhooks.jsx)15
-rw-r--r--webapp/components/integrations/components/integration_option.jsx (renamed from webapp/components/backstage/integration_option.jsx)0
-rw-r--r--webapp/components/integrations/components/integrations.jsx (renamed from webapp/components/backstage/integrations.jsx)13
-rw-r--r--webapp/components/logged_in.jsx7
-rw-r--r--webapp/components/navbar_dropdown.jsx20
-rw-r--r--webapp/components/post_view/components/post.jsx8
-rw-r--r--webapp/components/post_view/components/post_body.jsx9
-rw-r--r--webapp/components/post_view/components/post_body_additional_content.jsx3
-rw-r--r--webapp/components/post_view/components/post_list.jsx4
-rw-r--r--webapp/components/post_view/post_focus_view_controller.jsx14
-rw-r--r--webapp/components/post_view/post_view_controller.jsx14
-rw-r--r--webapp/components/root.jsx3
-rw-r--r--webapp/components/suggestion/emoticon_provider.jsx21
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);