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
Этот коммит содержится в:
коммит произвёл
Joram Wilander
родитель
a65f1fc266
Коммит
dc2f2a8001
@@ -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,6 +47,35 @@ export default class CustomEmojiSettings extends AdminSettings {
|
||||
}
|
||||
|
||||
renderSettings() {
|
||||
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={
|
||||
<FormattedMessage
|
||||
id='admin.customization.restrictCustomEmojiCreationTitle'
|
||||
defaultMessage='Restrict Custom Emoji Creation:'
|
||||
/>
|
||||
}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id='admin.customization.restrictCustomEmojiCreationDesc'
|
||||
defaultMessage='Restrict the creation of custom emoji to certain users.'
|
||||
/>
|
||||
}
|
||||
value={this.state.restrictCustomEmojiCreation}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.enableCustomEmoji}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsGroup>
|
||||
<BooleanSetting
|
||||
@@ -63,28 +95,7 @@ export default class CustomEmojiSettings extends AdminSettings {
|
||||
value={this.state.enableCustomEmoji}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<DropdownSetting
|
||||
id='restrictCustomEmojiCreation'
|
||||
values={[
|
||||
{value: 'all', text: Utils.localizeMessage('admin.customization.restrictCustomEmojiCreationAll', 'Allow everyone to create custom emoji')},
|
||||
{value: 'system_admin', text: Utils.localizeMessage('admin.customization.restrictCustomEmojiCreationSystemAdmin', 'Only allow system admins to create custom emoji')}
|
||||
]}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='admin.customization.restrictCustomEmojiCreationTitle'
|
||||
defaultMessage='Restrict Custom Emoji Creation:'
|
||||
/>
|
||||
}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id='admin.customization.restrictCustomEmojiCreationDesc'
|
||||
defaultMessage='Restrict the creation of custom emoji to certain users.'
|
||||
/>
|
||||
}
|
||||
value={this.state.restrictCustomEmojiCreation}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.enableCustomEmoji}
|
||||
/>
|
||||
{restrictSetting}
|
||||
</SettingsGroup>
|
||||
);
|
||||
}
|
||||
|
||||
71
webapp/components/backstage/backstage_controller.jsx
Обычный файл
71
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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'>
|
||||
108
webapp/components/backstage/components/backstage_list.jsx
Обычный файл
108
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 = (
|
||||
@@ -55,24 +93,31 @@ 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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
307
webapp/components/emoji/components/add_emoji.jsx
Обычный файл
307
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
218
webapp/components/emoji/components/emoji_list.jsx
Обычный файл
218
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
118
webapp/components/emoji/components/emoji_list_item.jsx
Обычный файл
118
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
/>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
Ссылка в новой задаче
Block a user