* 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
Этот коммит содержится в:
Harrison Healey
2016-07-05 11:58:18 -04:00
коммит произвёл Joram Wilander
родитель a65f1fc266
Коммит dc2f2a8001
56 изменённых файлов: 1697 добавлений и 8920 удалений

Просмотреть файл

@@ -180,12 +180,15 @@ ifeq ($(BUILD_ENTERPRISE_READY),true)
$(GO) test $(GOFLAGS) -run=$(TESTS) -covermode=count -c ./enterprise/ldap && ./ldap.test -test.v -test.timeout=120s -test.coverprofile=cldap.out || exit 1 $(GO) test $(GOFLAGS) -run=$(TESTS) -covermode=count -c ./enterprise/ldap && ./ldap.test -test.v -test.timeout=120s -test.coverprofile=cldap.out || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -covermode=count -c ./enterprise/compliance && ./compliance.test -test.v -test.timeout=120s -test.coverprofile=ccompliance.out || exit 1 $(GO) test $(GOFLAGS) -run=$(TESTS) -covermode=count -c ./enterprise/compliance && ./compliance.test -test.v -test.timeout=120s -test.coverprofile=ccompliance.out || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -covermode=count -c ./enterprise/emoji && ./emoji.test -test.v -test.timeout=120s -test.coverprofile=cemoji.out || exit 1
tail -n +2 cldap.out >> ecover.out tail -n +2 cldap.out >> ecover.out
tail -n +2 ccompliance.out >> ecover.out tail -n +2 ccompliance.out >> ecover.out
rm -f cldap.out ccompliance.out tail -n +2 cemoji.out >> ecover.out
rm -f cldap.out ccompliance.out cemoji.out
rm -r ldap.test rm -r ldap.test
rm -r compliance.test rm -r compliance.test
rm -r emoji.test
endif endif
internal-test-web-client: start-docker prepare-enterprise internal-test-web-client: start-docker prepare-enterprise

Просмотреть файл

@@ -16,6 +16,7 @@ import (
l4g "github.com/alecthomas/log4go" l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/mattermost/platform/einterfaces"
"github.com/mattermost/platform/model" "github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils" "github.com/mattermost/platform/utils"
) )
@@ -32,7 +33,7 @@ func InitEmoji() {
BaseRoutes.Emoji.Handle("/list", ApiUserRequired(getEmoji)).Methods("GET") BaseRoutes.Emoji.Handle("/list", ApiUserRequired(getEmoji)).Methods("GET")
BaseRoutes.Emoji.Handle("/create", ApiUserRequired(createEmoji)).Methods("POST") BaseRoutes.Emoji.Handle("/create", ApiUserRequired(createEmoji)).Methods("POST")
BaseRoutes.Emoji.Handle("/delete", ApiUserRequired(deleteEmoji)).Methods("POST") BaseRoutes.Emoji.Handle("/delete", ApiUserRequired(deleteEmoji)).Methods("POST")
BaseRoutes.Emoji.Handle("/{id:[A-Za-z0-9_]+}", ApiUserRequired(getEmojiImage)).Methods("GET") BaseRoutes.Emoji.Handle("/{id:[A-Za-z0-9_]+}", ApiUserRequiredTrustRequester(getEmojiImage)).Methods("GET")
} }
func getEmoji(c *Context, w http.ResponseWriter, r *http.Request) { func getEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -58,7 +59,8 @@ func createEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
return return
} }
if !(*utils.Cfg.ServiceSettings.RestrictCustomEmojiCreation == model.RESTRICT_EMOJI_CREATION_ALL || c.IsSystemAdmin()) { if emojiInterface := einterfaces.GetEmojiInterface(); emojiInterface != nil &&
!emojiInterface.CanUserCreateEmoji(c.Session.Roles, c.Session.TeamMembers) {
c.Err = model.NewLocAppError("createEmoji", "api.emoji.create.permissions.app_error", nil, "user_id="+c.Session.UserId) c.Err = model.NewLocAppError("createEmoji", "api.emoji.create.permissions.app_error", nil, "user_id="+c.Session.UserId)
c.Err.StatusCode = http.StatusUnauthorized c.Err.StatusCode = http.StatusUnauthorized
return return

Просмотреть файл

@@ -22,6 +22,12 @@ func TestGetEmoji(t *testing.T) {
th := Setup().InitBasic() th := Setup().InitBasic()
Client := th.BasicClient Client := th.BasicClient
EnableCustomEmoji := *utils.Cfg.ServiceSettings.EnableCustomEmoji
defer func() {
*utils.Cfg.ServiceSettings.EnableCustomEmoji = EnableCustomEmoji
}()
*utils.Cfg.ServiceSettings.EnableCustomEmoji = true
emojis := []*model.Emoji{ emojis := []*model.Emoji{
{ {
CreatorId: model.NewId(), CreatorId: model.NewId(),
@@ -95,13 +101,10 @@ func TestCreateEmoji(t *testing.T) {
Client := th.BasicClient Client := th.BasicClient
EnableCustomEmoji := *utils.Cfg.ServiceSettings.EnableCustomEmoji EnableCustomEmoji := *utils.Cfg.ServiceSettings.EnableCustomEmoji
RestrictCustomEmojiCreation := *utils.Cfg.ServiceSettings.RestrictCustomEmojiCreation
defer func() { defer func() {
*utils.Cfg.ServiceSettings.EnableCustomEmoji = EnableCustomEmoji *utils.Cfg.ServiceSettings.EnableCustomEmoji = EnableCustomEmoji
*utils.Cfg.ServiceSettings.RestrictCustomEmojiCreation = RestrictCustomEmojiCreation
}() }()
*utils.Cfg.ServiceSettings.EnableCustomEmoji = false *utils.Cfg.ServiceSettings.EnableCustomEmoji = false
*utils.Cfg.ServiceSettings.RestrictCustomEmojiCreation = model.RESTRICT_EMOJI_CREATION_ALL
emoji := &model.Emoji{ emoji := &model.Emoji{
CreatorId: th.BasicUser.Id, CreatorId: th.BasicUser.Id,
@@ -213,28 +216,6 @@ func TestCreateEmoji(t *testing.T) {
if _, err := Client.CreateEmoji(emoji, createTestGif(t, 10, 10), "image.gif"); err == nil { if _, err := Client.CreateEmoji(emoji, createTestGif(t, 10, 10), "image.gif"); err == nil {
t.Fatal("shouldn't be able to create an emoji as another user") t.Fatal("shouldn't be able to create an emoji as another user")
} }
*utils.Cfg.ServiceSettings.RestrictCustomEmojiCreation = model.RESTRICT_EMOJI_CREATION_ADMIN
// try to create an emoji when only system admins are allowed to create them
emoji = &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
if _, err := Client.CreateEmoji(emoji, createTestGif(t, 10, 10), "image.gif"); err == nil {
t.Fatal("shouldn't be able to create an emoji when not a system admin")
}
emoji = &model.Emoji{
CreatorId: th.SystemAdminUser.Id,
Name: model.NewId(),
}
if emojiResult, err := th.SystemAdminClient.CreateEmoji(emoji, createTestPng(t, 10, 10), "image.png"); err != nil {
t.Fatal(err)
} else {
emoji = emojiResult
}
th.SystemAdminClient.MustGeneric(th.SystemAdminClient.DeleteEmoji(emoji.Id))
} }
func TestDeleteEmoji(t *testing.T) { func TestDeleteEmoji(t *testing.T) {

Просмотреть файл

@@ -5,9 +5,9 @@
"SegmentDeveloperKey": "", "SegmentDeveloperKey": "",
"GoogleDeveloperKey": "", "GoogleDeveloperKey": "",
"EnableOAuthServiceProvider": false, "EnableOAuthServiceProvider": false,
"EnableIncomingWebhooks": false, "EnableIncomingWebhooks": true,
"EnableOutgoingWebhooks": false, "EnableOutgoingWebhooks": true,
"EnableCommands": false, "EnableCommands": true,
"EnableOnlyAdminIntegrations": true, "EnableOnlyAdminIntegrations": true,
"EnablePostUsernameOverride": false, "EnablePostUsernameOverride": false,
"EnablePostIconOverride": false, "EnablePostIconOverride": false,
@@ -24,7 +24,7 @@
"WebsocketSecurePort": 443, "WebsocketSecurePort": 443,
"WebsocketPort": 80, "WebsocketPort": 80,
"WebserverMode": "regular", "WebserverMode": "regular",
"EnableCustomEmoji": true, "EnableCustomEmoji": false,
"RestrictCustomEmojiCreation": "all" "RestrictCustomEmojiCreation": "all"
}, },
"TeamSettings": { "TeamSettings": {

22
einterfaces/emoji.go Обычный файл
Просмотреть файл

@@ -0,0 +1,22 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package einterfaces
import (
"github.com/mattermost/platform/model"
)
type EmojiInterface interface {
CanUserCreateEmoji(string, []*model.TeamMember) bool
}
var theEmojiInterface EmojiInterface
func RegisterEmojiInterface(newInterface EmojiInterface) {
theEmojiInterface = newInterface
}
func GetEmojiInterface() EmojiInterface {
return theEmojiInterface
}

Просмотреть файл

@@ -581,7 +581,7 @@
}, },
{ {
"id": "api.emoji.create.parse.app_error", "id": "api.emoji.create.parse.app_error",
"translation": "Unable to create emoji. Image exceeds maximum file size." "translation": "Unable to create emoji. Could not understand request."
}, },
{ {
"id": "api.emoji.create.permissions.app_error", "id": "api.emoji.create.permissions.app_error",
@@ -589,7 +589,7 @@
}, },
{ {
"id": "api.emoji.create.too_large.app_error", "id": "api.emoji.create.too_large.app_error",
"translation": "Unable to create emoji. Could not understand request." "translation": "Unable to create emoji. Image must be less than 64 KB in size."
}, },
{ {
"id": "api.emoji.delete.permissions.app_error", "id": "api.emoji.delete.permissions.app_error",
@@ -621,7 +621,7 @@
}, },
{ {
"id": "api.emoji.upload.large_image.app_error", "id": "api.emoji.upload.large_image.app_error",
"translation": "Unable to create emoji. Image exceeds maximum dimensions." "translation": "Unable to create emoji. Image must be at most 128 by 128 pixels."
}, },
{ {
"id": "api.export.json.app_error", "id": "api.export.json.app_error",
@@ -2079,6 +2079,10 @@
"id": "ent.compliance.run_started.info", "id": "ent.compliance.run_started.info",
"translation": "Compliance export started for job '{{.JobName}}' at '{{.FilePath}}'" "translation": "Compliance export started for job '{{.JobName}}' at '{{.FilePath}}'"
}, },
{
"id": "ent.emoji.licence_disable.app_error",
"translation": "Custom emoji restrictions disabled by current license. Please contact your system administrator about upgrading your enterprise license."
},
{ {
"id": "ent.ldap.do_login.bind_admin_user.app_error", "id": "ent.ldap.do_login.bind_admin_user.app_error",
"translation": "Unable to bind to LDAP server. Check BindUsername and BindPassword." "translation": "Unable to bind to LDAP server. Check BindUsername and BindPassword."

Просмотреть файл

@@ -38,8 +38,9 @@ const (
FAKE_SETTING = "********************************" FAKE_SETTING = "********************************"
RESTRICT_EMOJI_CREATION_ALL = "all" RESTRICT_EMOJI_CREATION_ALL = "all"
RESTRICT_EMOJI_CREATION_ADMIN = "system_admin" RESTRICT_EMOJI_CREATION_ADMIN = "admin"
RESTRICT_EMOJI_CREATION_SYSTEM_ADMIN = "system_admin"
) )
type ServiceSettings struct { type ServiceSettings struct {

Просмотреть файл

@@ -27,7 +27,10 @@ export default class CustomEmojiSettings extends AdminSettings {
getConfigFromState(config) { getConfigFromState(config) {
config.ServiceSettings.EnableCustomEmoji = this.state.enableCustomEmoji; 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; return config;
} }
@@ -44,6 +47,35 @@ export default class CustomEmojiSettings extends AdminSettings {
} }
renderSettings() { 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 ( return (
<SettingsGroup> <SettingsGroup>
<BooleanSetting <BooleanSetting
@@ -63,28 +95,7 @@ export default class CustomEmojiSettings extends AdminSettings {
value={this.state.enableCustomEmoji} value={this.state.enableCustomEmoji}
onChange={this.handleChange} onChange={this.handleChange}
/> />
<DropdownSetting {restrictSetting}
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}
/>
</SettingsGroup> </SettingsGroup>
); );
} }

Просмотреть файл

@@ -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} to={link}
className='category-title' className='category-title'
activeClassName='category-title--active' activeClassName='category-title--active'
onlyActiveOnIndex={true}
> >
<i className={'fa ' + icon}/> <i className={'fa ' + icon}/>
<span className='category-title__text'> <span className='category-title__text'>

Просмотреть файл

@@ -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. // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
import $ from 'jquery';
import React from 'react'; import React from 'react';
import TeamStore from 'stores/team_store.jsx';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import {Link} from 'react-router/es6'; import {Link} from 'react-router/es6';
export default class BackstageNavbar extends React.Component { export default class BackstageNavbar extends React.Component {
constructor(props) { static get propTypes() {
super(props); return {
team: React.propTypes.object.isRequired
this.handleChange = this.handleChange.bind(this);
this.state = {
team: TeamStore.getCurrent()
}; };
} }
componentDidMount() {
TeamStore.addChangeListener(this.handleChange);
$('body').addClass('backstage');
}
componentWillUnmount() {
TeamStore.removeChangeListener(this.handleChange);
$('body').removeClass('backstage');
}
handleChange() {
this.setState({
team: TeamStore.getCurrent()
});
}
render() { render() {
if (!this.state.team) { if (!this.props.team) {
return null; return null;
} }
return ( return (
<div className='backstage-navbar row'> <div className='backstage-navbar'>
<Link <Link
className='backstage-navbar__back' 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'/> <i className='fa fa-angle-left'/>
<span> <span>

Просмотреть файл

@@ -3,13 +3,51 @@
import React from 'react'; 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 BackstageCategory from './backstage_category.jsx';
import BackstageSection from './backstage_section.jsx'; import BackstageSection from './backstage_section.jsx';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
export default class BackstageSidebar extends React.Component { 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; let incomingWebhooks = null;
if (window.mm_config.EnableIncomingWebhooks === 'true') { if (window.mm_config.EnableIncomingWebhooks === 'true') {
incomingWebhooks = ( 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 ( return (
<div className='backstage-sidebar'> <div className='backstage-sidebar'>
<ul> <ul>
<BackstageCategory {this.renderCustomEmoji()}
name='integrations' {this.renderIntegrations()}
parentLink={'/' + Utils.getTeamNameFromUrl() + '/settings'}
icon='fa-link'
title={
<FormattedMessage
id='backstage_sidebar.integrations'
defaultMessage='Integrations'
/>
}
>
{incomingWebhooks}
{outgoingWebhooks}
{commands}
</BackstageCategory>
</ul> </ul>
</div> </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>
);
}
}

Просмотреть файл

@@ -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>
);
}
}

Просмотреть файл

@@ -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>
);
}
}

Просмотреть файл

@@ -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 AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.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 {FormattedMessage} from 'react-intl';
import FormError from 'components/form_error.jsx'; import FormError from 'components/form_error.jsx';
import {browserHistory, Link} from 'react-router/es6'; import {browserHistory, Link} from 'react-router/es6';
@@ -17,6 +17,12 @@ const REQUEST_POST = 'P';
const REQUEST_GET = 'G'; const REQUEST_GET = 'G';
export default class AddCommand extends React.Component { export default class AddCommand extends React.Component {
static get propTypes() {
return {
team: React.propTypes.object.isRequired
};
}
constructor(props) { constructor(props) {
super(props); super(props);
@@ -155,7 +161,7 @@ export default class AddCommand extends React.Component {
AsyncClient.addCommand( AsyncClient.addCommand(
command, command,
() => { () => {
browserHistory.push('/' + Utils.getTeamNameFromUrl() + '/settings/integrations/commands'); browserHistory.push('/' + this.props.team.name + '/integrations/commands');
}, },
(err) => { (err) => {
this.setState({ this.setState({
@@ -300,7 +306,7 @@ export default class AddCommand extends React.Component {
return ( return (
<div className='backstage-content row'> <div className='backstage-content row'>
<BackstageHeader> <BackstageHeader>
<Link to={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/commands'}> <Link to={'/' + this.props.team.name + '/integrations/commands'}>
<FormattedMessage <FormattedMessage
id='installed_command.header' id='installed_command.header'
defaultMessage='Slash Commands' defaultMessage='Slash Commands'
@@ -312,7 +318,10 @@ export default class AddCommand extends React.Component {
/> />
</BackstageHeader> </BackstageHeader>
<div className='backstage-form'> <div className='backstage-form'>
<form className='form-horizontal'> <form
className='form-horizontal'
onSubmit={this.handleSubmit}
>
<div className='form-group'> <div className='form-group'>
<label <label
className='control-label col-sm-4' 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]}/> <FormError errors={[this.state.serverError, this.state.clientError]}/>
<Link <Link
className='btn btn-sm' className='btn btn-sm'
to={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/commands'} to={'/' + this.props.team.name + '/integrations/commands'}
> >
<FormattedMessage <FormattedMessage
id='add_command.cancel' id='add_command.cancel'

Просмотреть файл

@@ -4,9 +4,8 @@
import React from 'react'; import React from 'react';
import * as AsyncClient from 'utils/async_client.jsx'; 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 ChannelSelect from 'components/channel_select.jsx';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import FormError from 'components/form_error.jsx'; 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'; import SpinnerButton from 'components/spinner_button.jsx';
export default class AddIncomingWebhook extends React.Component { export default class AddIncomingWebhook extends React.Component {
static get propTypes() {
return {
team: React.propTypes.object.isRequired
};
}
constructor(props) { constructor(props) {
super(props); super(props);
@@ -69,7 +74,7 @@ export default class AddIncomingWebhook extends React.Component {
AsyncClient.addIncomingHook( AsyncClient.addIncomingHook(
hook, hook,
() => { () => {
browserHistory.push('/' + Utils.getTeamNameFromUrl() + '/settings/integrations/incoming_webhooks'); browserHistory.push('/' + this.props.team.name + '/integrations/incoming_webhooks');
}, },
(err) => { (err) => {
this.setState({ this.setState({
@@ -102,7 +107,7 @@ export default class AddIncomingWebhook extends React.Component {
return ( return (
<div className='backstage-content'> <div className='backstage-content'>
<BackstageHeader> <BackstageHeader>
<Link to={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/incoming_webhooks'}> <Link to={'/' + this.props.team.name + '/integrations/incoming_webhooks'}>
<FormattedMessage <FormattedMessage
id='installed_incoming_webhooks.header' id='installed_incoming_webhooks.header'
defaultMessage='Incoming Webhooks' defaultMessage='Incoming Webhooks'
@@ -114,7 +119,10 @@ export default class AddIncomingWebhook extends React.Component {
/> />
</BackstageHeader> </BackstageHeader>
<div className='backstage-form'> <div className='backstage-form'>
<form className='form-horizontal'> <form
className='form-horizontal'
onSubmit={this.handleSubmit}
>
<div className='form-group'> <div className='form-group'>
<label <label
className='control-label col-sm-4' 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]}/> <FormError errors={[this.state.serverError, this.state.clientError]}/>
<Link <Link
className='btn btn-sm' className='btn btn-sm'
to={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/incoming_webhooks'} to={'/' + this.props.team.name + '/integrations/incoming_webhooks'}
> >
<FormattedMessage <FormattedMessage
id='add_incoming_webhook.cancel' id='add_incoming_webhook.cancel'

Просмотреть файл

@@ -4,9 +4,8 @@
import React from 'react'; import React from 'react';
import * as AsyncClient from 'utils/async_client.jsx'; 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 ChannelSelect from 'components/channel_select.jsx';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import FormError from 'components/form_error.jsx'; 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'; import SpinnerButton from 'components/spinner_button.jsx';
export default class AddOutgoingWebhook extends React.Component { export default class AddOutgoingWebhook extends React.Component {
static get propTypes() {
return {
team: React.propTypes.object.isRequired
};
}
constructor(props) { constructor(props) {
super(props); super(props);
@@ -112,7 +117,7 @@ export default class AddOutgoingWebhook extends React.Component {
AsyncClient.addOutgoingHook( AsyncClient.addOutgoingHook(
hook, hook,
() => { () => {
browserHistory.push('/' + Utils.getTeamNameFromUrl() + '/settings/integrations/outgoing_webhooks'); browserHistory.push('/' + this.props.team.name + '/integrations/outgoing_webhooks');
}, },
(err) => { (err) => {
this.setState({ this.setState({
@@ -165,7 +170,7 @@ export default class AddOutgoingWebhook extends React.Component {
return ( return (
<div className='backstage-content'> <div className='backstage-content'>
<BackstageHeader> <BackstageHeader>
<Link to={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/outgoing_webhooks'}> <Link to={'/' + this.props.team.name + '/integrations/outgoing_webhooks'}>
<FormattedMessage <FormattedMessage
id='installed_outgoing_webhooks.header' id='installed_outgoing_webhooks.header'
defaultMessage='Outgoing Webhooks' defaultMessage='Outgoing Webhooks'
@@ -177,7 +182,10 @@ export default class AddOutgoingWebhook extends React.Component {
/> />
</BackstageHeader> </BackstageHeader>
<div className='backstage-form'> <div className='backstage-form'>
<form className='form-horizontal'> <form
className='form-horizontal'
onSubmit={this.handleSubmit}
>
<div className='form-group'> <div className='form-group'>
<label <label
className='control-label col-sm-4' 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]}/> <FormError errors={[this.state.serverError, this.state.clientError]}/>
<Link <Link
className='btn btn-sm' className='btn btn-sm'
to={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/outgoing_webhooks'} to={'/' + this.props.team.name + '/integrations/outgoing_webhooks'}
> >
<FormattedMessage <FormattedMessage
id='add_outgoing_webhook.cancel' id='add_outgoing_webhook.cancel'

Просмотреть файл

@@ -50,8 +50,9 @@ export default class InstalledCommand extends React.Component {
render() { render() {
const command = this.props.command; 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; return null;
} }
@@ -61,7 +62,7 @@ export default class InstalledCommand extends React.Component {
} else { } else {
name = ( name = (
<FormattedMessage <FormattedMessage
id='installed_integraions.unnamed_command' id='installed_commands.unnamed_command'
defaultMessage='Unnamed Slash Command' defaultMessage='Unnamed Slash Command'
/> />
); );

Просмотреть файл

@@ -8,11 +8,17 @@ import IntegrationStore from 'stores/integration_store.jsx';
import TeamStore from 'stores/team_store.jsx'; import TeamStore from 'stores/team_store.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.jsx';
import BackstageList from 'components/backstage/components/backstage_list.jsx';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import InstalledCommand from './installed_command.jsx'; import InstalledCommand from './installed_command.jsx';
import InstalledIntegrations from './installed_integrations.jsx';
export default class InstalledCommands extends React.Component { export default class InstalledCommands extends React.Component {
static get propTypes() {
return {
team: React.propTypes.object.isRequired
};
}
constructor(props) { constructor(props) {
super(props); super(props);
@@ -71,7 +77,7 @@ export default class InstalledCommands extends React.Component {
}); });
return ( return (
<InstalledIntegrations <BackstageList
header={ header={
<FormattedMessage <FormattedMessage
id='installed_commands.header' id='installed_commands.header'
@@ -84,17 +90,18 @@ export default class InstalledCommands extends React.Component {
defaultMessage='Add Slash Command' defaultMessage='Add Slash Command'
/> />
} }
addLink={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/commands/add'} addLink={'/' + this.props.team.name + '/integrations/commands/add'}
emptyText={ emptyText={
<FormattedMessage <FormattedMessage
id='installed_commands.empty' id='installed_commands.empty'
defaultMessage='No slash commands found' defaultMessage='No slash commands found'
/> />
} }
searchPlaceholder={Utils.localizeMessage('installed_commands.search', 'Search Slash Commands')}
loading={this.state.loading} loading={this.state.loading}
> >
{commands} {commands}
</InstalledIntegrations> </BackstageList>
); );
} }
} }

Просмотреть файл

@@ -51,8 +51,9 @@ export default class InstalledIncomingWebhook extends React.Component {
render() { render() {
const incomingWebhook = this.props.incomingWebhook; const incomingWebhook = this.props.incomingWebhook;
const channel = ChannelStore.get(incomingWebhook.channel_id); 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; return null;
} }

Просмотреть файл

@@ -8,11 +8,17 @@ import IntegrationStore from 'stores/integration_store.jsx';
import TeamStore from 'stores/team_store.jsx'; import TeamStore from 'stores/team_store.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.jsx';
import BackstageList from 'components/backstage/components/backstage_list.jsx';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import InstalledIncomingWebhook from './installed_incoming_webhook.jsx'; import InstalledIncomingWebhook from './installed_incoming_webhook.jsx';
import InstalledIntegrations from './installed_integrations.jsx';
export default class InstalledIncomingWebhooks extends React.Component { export default class InstalledIncomingWebhooks extends React.Component {
static get propTypes() {
return {
team: React.propTypes.object.isRequired
};
}
constructor(props) { constructor(props) {
super(props); super(props);
@@ -65,7 +71,7 @@ export default class InstalledIncomingWebhooks extends React.Component {
}); });
return ( return (
<InstalledIntegrations <BackstageList
header={ header={
<FormattedMessage <FormattedMessage
id='installed_incoming_webhooks.header' id='installed_incoming_webhooks.header'
@@ -78,17 +84,18 @@ export default class InstalledIncomingWebhooks extends React.Component {
defaultMessage='Add Incoming Webhook' defaultMessage='Add Incoming Webhook'
/> />
} }
addLink={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/incoming_webhooks/add'} addLink={'/' + this.props.team.name + '/integrations/incoming_webhooks/add'}
emptyText={ emptyText={
<FormattedMessage <FormattedMessage
id='installed_incoming_webhooks.empty' id='installed_incoming_webhooks.empty'
defaultMessage='No incoming webhooks found' defaultMessage='No incoming webhooks found'
/> />
} }
searchPlaceholder={Utils.localizeMessage('installed_incoming_webhooks.search', 'Search Incoming Webhooks')}
loading={this.state.loading} loading={this.state.loading}
> >
{incomingWebhooks} {incomingWebhooks}
</InstalledIntegrations> </BackstageList>
); );
} }
} }

Просмотреть файл

@@ -65,8 +65,9 @@ export default class InstalledOutgoingWebhook extends React.Component {
render() { render() {
const outgoingWebhook = this.props.outgoingWebhook; const outgoingWebhook = this.props.outgoingWebhook;
const channel = ChannelStore.get(outgoingWebhook.channel_id); 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; return null;
} }

Просмотреть файл

@@ -8,11 +8,17 @@ import IntegrationStore from 'stores/integration_store.jsx';
import TeamStore from 'stores/team_store.jsx'; import TeamStore from 'stores/team_store.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.jsx';
import BackstageList from 'components/backstage/components/backstage_list.jsx';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import InstalledOutgoingWebhook from './installed_outgoing_webhook.jsx'; import InstalledOutgoingWebhook from './installed_outgoing_webhook.jsx';
import InstalledIntegrations from './installed_integrations.jsx';
export default class InstalledOutgoingWebhooks extends React.Component { export default class InstalledOutgoingWebhooks extends React.Component {
static get propTypes() {
return {
team: React.propTypes.object.isRequired
};
}
constructor(props) { constructor(props) {
super(props); super(props);
@@ -71,7 +77,7 @@ export default class InstalledOutgoingWebhooks extends React.Component {
}); });
return ( return (
<InstalledIntegrations <BackstageList
header={ header={
<FormattedMessage <FormattedMessage
id='installed_outgoing_webhooks.header' id='installed_outgoing_webhooks.header'
@@ -84,17 +90,18 @@ export default class InstalledOutgoingWebhooks extends React.Component {
defaultMessage='Add Outgoing Webhook' defaultMessage='Add Outgoing Webhook'
/> />
} }
addLink={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/outgoing_webhooks/add'} addLink={'/' + this.props.team.name + '/integrations/outgoing_webhooks/add'}
emptyText={ emptyText={
<FormattedMessage <FormattedMessage
id='installed_outgoing_webhooks.empty' id='installed_outgoing_webhooks.empty'
defaultMessage='No outgoing webhooks found' defaultMessage='No outgoing webhooks found'
/> />
} }
searchPlaceholder={Utils.localizeMessage('installed_outgoing_webhooks.search', 'Search Outgoing Webhooks')}
loading={this.state.loading} loading={this.state.loading}
> >
{outgoingWebhooks} {outgoingWebhooks}
</InstalledIntegrations> </BackstageList>
); );
} }
} }

Просмотреть файл

@@ -5,11 +5,16 @@ import React from 'react';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import IntegrationOption from './integration_option.jsx'; import IntegrationOption from './integration_option.jsx';
import * as Utils from 'utils/utils.jsx';
import WebhookIcon from 'images/webhook_icon.jpg'; import WebhookIcon from 'images/webhook_icon.jpg';
export default class Integrations extends React.Component { export default class Integrations extends React.Component {
static get propTypes() {
return {
team: React.propTypes.object.isRequired
};
}
render() { render() {
const options = []; const options = [];
@@ -30,7 +35,7 @@ export default class Integrations extends React.Component {
defaultMessage='Incoming webhooks allow external integrations to send messages' 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' 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' 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(); e.preventDefault();
} }
}); });
// Get custom emoji from the server
if (window.mm_config.EnableCustomEmoji === 'true') {
AsyncClient.listEmoji();
}
} }
componentWillUnmount() { componentWillUnmount() {

Просмотреть файл

@@ -85,6 +85,7 @@ export default class NavbarDropdown extends React.Component {
var isSystemAdmin = false; var isSystemAdmin = false;
var teamSettings = null; var teamSettings = null;
let integrationsLink = null; let integrationsLink = null;
let customEmojiLink = null;
if (currentUser != null) { if (currentUser != null) {
isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser(); isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
@@ -166,7 +167,7 @@ export default class NavbarDropdown extends React.Component {
if (integrationsEnabled && (isAdmin || window.mm_config.EnableOnlyAdminIntegrations !== 'true')) { if (integrationsEnabled && (isAdmin || window.mm_config.EnableOnlyAdminIntegrations !== 'true')) {
integrationsLink = ( integrationsLink = (
<li> <li>
<Link to={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations'}> <Link to={'/' + Utils.getTeamNameFromUrl() + '/integrations'}>
<FormattedMessage <FormattedMessage
id='navbar_dropdown.integrations' id='navbar_dropdown.integrations'
defaultMessage='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) { if (isSystemAdmin) {
sysAdminLink = ( sysAdminLink = (
<li> <li>
@@ -327,8 +341,10 @@ export default class NavbarDropdown extends React.Component {
</a> </a>
</li> </li>
<li className='divider'></li> <li className='divider'></li>
{teamSettings}
{integrationsLink} {integrationsLink}
{customEmojiLink}
<li className='divider'></li>
{teamSettings}
{manageLink} {manageLink}
{sysAdminLink} {sysAdminLink}
{teams} {teams}

Просмотреть файл

@@ -84,6 +84,10 @@ export default class Post extends React.Component {
return true; return true;
} }
if (nextProps.emojis !== this.props.emojis) {
return true;
}
return false; return false;
} }
render() { render() {
@@ -200,6 +204,7 @@ export default class Post extends React.Component {
handleCommentClick={this.handleCommentClick} handleCommentClick={this.handleCommentClick}
compactDisplay={this.props.compactDisplay} compactDisplay={this.props.compactDisplay}
previewCollapsed={this.props.previewCollapsed} previewCollapsed={this.props.previewCollapsed}
emojis={this.props.emojis}
/> />
</div> </div>
</div> </div>
@@ -225,5 +230,6 @@ Post.propTypes = {
compactDisplay: React.PropTypes.bool, compactDisplay: React.PropTypes.bool,
previewCollapsed: React.PropTypes.string, previewCollapsed: React.PropTypes.string,
commentCount: React.PropTypes.number, 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; return true;
} }
if (nextProps.emojis !== this.props.emojis) {
return true;
}
return false; return false;
} }
@@ -151,7 +155,7 @@ export default class PostBody extends React.Component {
message = ( message = (
<span <span
onClick={TextFormatting.handleClick} 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, retryPost: React.PropTypes.func.isRequired,
handleCommentClick: React.PropTypes.func.isRequired, handleCommentClick: React.PropTypes.func.isRequired,
compactDisplay: React.PropTypes.bool, 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)) { if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) {
return true; return true;
} }
if (!Utils.areObjectsEqual(nextProps.message, this.props.message)) {
return true;
}
if (nextState.embedVisible !== this.state.embedVisible) { if (nextState.embedVisible !== this.state.embedVisible) {
return true; return true;
} }

Просмотреть файл

@@ -265,6 +265,7 @@ export default class PostList extends React.Component {
compactDisplay={this.props.compactDisplay} compactDisplay={this.props.compactDisplay}
previewCollapsed={this.props.previewsCollapsed} previewCollapsed={this.props.previewsCollapsed}
useMilitaryTime={this.props.useMilitaryTime} useMilitaryTime={this.props.useMilitaryTime}
emojis={this.props.emojis}
/> />
); );
@@ -527,5 +528,6 @@ PostList.propTypes = {
compactDisplay: React.PropTypes.bool, compactDisplay: React.PropTypes.bool,
previewsCollapsed: React.PropTypes.string, previewsCollapsed: React.PropTypes.string,
useMilitaryTime: React.PropTypes.bool.isRequired, 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 PostList from './components/post_list.jsx';
import LoadingScreen from 'components/loading_screen.jsx'; import LoadingScreen from 'components/loading_screen.jsx';
import EmojiStore from 'stores/emoji_store.jsx';
import PostStore from 'stores/post_store.jsx'; import PostStore from 'stores/post_store.jsx';
import UserStore from 'stores/user_store.jsx'; import UserStore from 'stores/user_store.jsx';
import ChannelStore from 'stores/channel_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.onChannelChange = this.onChannelChange.bind(this);
this.onPostsChange = this.onPostsChange.bind(this); this.onPostsChange = this.onPostsChange.bind(this);
this.onUserChange = this.onUserChange.bind(this); this.onUserChange = this.onUserChange.bind(this);
this.onEmojiChange = this.onEmojiChange.bind(this);
this.onPostListScroll = this.onPostListScroll.bind(this); this.onPostListScroll = this.onPostListScroll.bind(this);
const focusedPostId = PostStore.getFocusedPostId(); const focusedPostId = PostStore.getFocusedPostId();
@@ -38,7 +40,8 @@ export default class PostFocusView extends React.Component {
currentChannel: ChannelStore.getCurrentId().slice(), currentChannel: ChannelStore.getCurrentId().slice(),
scrollPostId: focusedPostId, scrollPostId: focusedPostId,
atTop: PostStore.getVisibilityAtTop(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); ChannelStore.addChangeListener(this.onChannelChange);
PostStore.addChangeListener(this.onPostsChange); PostStore.addChangeListener(this.onPostsChange);
UserStore.addChangeListener(this.onUserChange); UserStore.addChangeListener(this.onUserChange);
EmojiStore.addChangeListener(this.onEmojiChange);
} }
componentWillUnmount() { componentWillUnmount() {
ChannelStore.removeChangeListener(this.onChannelChange); ChannelStore.removeChangeListener(this.onChannelChange);
PostStore.removeChangeListener(this.onPostsChange); PostStore.removeChangeListener(this.onPostsChange);
UserStore.removeChangeListener(this.onUserChange); UserStore.removeChangeListener(this.onUserChange);
EmojiStore.removeChangeListener(this.onEmojiChange);
} }
onChannelChange() { onChannelChange() {
@@ -87,6 +92,12 @@ export default class PostFocusView extends React.Component {
this.setState({currentUser: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(profiles))}); this.setState({currentUser: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(profiles))});
} }
onEmojiChange() {
this.setState({
emojis: EmojiStore.getEmojis()
});
}
onPostListScroll() { onPostListScroll() {
this.setState({scrollType: ScrollTypes.FREE}); this.setState({scrollType: ScrollTypes.FREE});
} }
@@ -116,6 +127,7 @@ export default class PostFocusView extends React.Component {
showMoreMessagesBottom={!this.state.atBottom} showMoreMessagesBottom={!this.state.atBottom}
postsToHighlight={postsToHighlight} postsToHighlight={postsToHighlight}
isFocusPost={true} isFocusPost={true}
emojis={this.state.emojis}
/> />
); );
} }

Просмотреть файл

@@ -4,6 +4,7 @@
import PostList from './components/post_list.jsx'; import PostList from './components/post_list.jsx';
import LoadingScreen from 'components/loading_screen.jsx'; import LoadingScreen from 'components/loading_screen.jsx';
import EmojiStore from 'stores/emoji_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx';
import UserStore from 'stores/user_store.jsx'; import UserStore from 'stores/user_store.jsx';
import PostStore from 'stores/post_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.onPreferenceChange = this.onPreferenceChange.bind(this);
this.onUserChange = this.onUserChange.bind(this); this.onUserChange = this.onUserChange.bind(this);
this.onPostsChange = this.onPostsChange.bind(this); this.onPostsChange = this.onPostsChange.bind(this);
this.onEmojisChange = this.onEmojisChange.bind(this);
this.onPostsViewJumpRequest = this.onPostsViewJumpRequest.bind(this); this.onPostsViewJumpRequest = this.onPostsViewJumpRequest.bind(this);
this.onPostListScroll = this.onPostListScroll.bind(this); this.onPostListScroll = this.onPostListScroll.bind(this);
this.onActivate = this.onActivate.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, 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, 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'), 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() { onActivate() {
PreferenceStore.addChangeListener(this.onPreferenceChange); PreferenceStore.addChangeListener(this.onPreferenceChange);
UserStore.addChangeListener(this.onUserChange); UserStore.addChangeListener(this.onUserChange);
PostStore.addChangeListener(this.onPostsChange); PostStore.addChangeListener(this.onPostsChange);
PostStore.addPostsViewJumpListener(this.onPostsViewJumpRequest); PostStore.addPostsViewJumpListener(this.onPostsViewJumpRequest);
EmojiStore.addChangeListener(this.onEmojisChange);
} }
onDeactivate() { onDeactivate() {
@@ -114,6 +124,7 @@ export default class PostViewController extends React.Component {
UserStore.removeChangeListener(this.onUserChange); UserStore.removeChangeListener(this.onUserChange);
PostStore.removeChangeListener(this.onPostsChange); PostStore.removeChangeListener(this.onPostsChange);
PostStore.removePostsViewJumpListener(this.onPostsViewJumpRequest); PostStore.removePostsViewJumpListener(this.onPostsViewJumpRequest);
EmojiStore.removeChangeListener(this.onEmojisChange);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
@@ -265,6 +276,7 @@ export default class PostViewController extends React.Component {
previewsCollapsed={this.state.previewsCollapsed} previewsCollapsed={this.state.previewsCollapsed}
useMilitaryTime={this.state.useMilitaryTime} useMilitaryTime={this.state.useMilitaryTime}
lastViewed={this.state.lastViewed} lastViewed={this.state.lastViewed}
emojis={this.state.emojis}
/> />
); );
} }

Просмотреть файл

@@ -1,9 +1,6 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // 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 * as GlobalActions from 'actions/global_actions.jsx';
import LocalizationStore from 'stores/localization_store.jsx'; import LocalizationStore from 'stores/localization_store.jsx';
import Client from 'utils/web_client.jsx'; import Client from 'utils/web_client.jsx';

Просмотреть файл

@@ -3,6 +3,7 @@
import React from 'react'; import React from 'react';
import EmojiStore from 'stores/emoji_store.jsx';
import * as Emoticons from 'utils/emoticons.jsx'; import * as Emoticons from 'utils/emoticons.jsx';
import SuggestionStore from 'stores/suggestion_store.jsx'; import SuggestionStore from 'stores/suggestion_store.jsx';
@@ -29,7 +30,7 @@ class EmoticonSuggestion extends Suggestion {
<img <img
alt={text} alt={text}
className='emoticon-suggestion__image' className='emoticon-suggestion__image'
src={emoticon.path} src={EmojiStore.getEmojiImageUrl(emoticon)}
title={text} title={text}
/> />
</div> </div>
@@ -53,21 +54,19 @@ export default class EmoticonProvider {
const matched = []; const matched = [];
const emoticons = Emoticons.getEmoticonsByName();
// check for text emoticons // check for text emoticons
for (const emoticon of Object.keys(Emoticons.emoticonPatterns)) { for (const emoticon of Object.keys(Emoticons.emoticonPatterns)) {
if (Emoticons.emoticonPatterns[emoticon].test(text)) { 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; hasSuggestions = true;
} }
} }
// checked for named emoji // check for named emoji
for (const [name, emoticon] of emoticons) { for (const [name, emoji] of EmojiStore.getEmojis()) {
if (name.indexOf(partialName) !== -1) { if (name.indexOf(partialName) !== -1) {
matched.push(emoticon); matched.push(emoji);
if (matched.length >= MAX_EMOTICON_SUGGESTIONS) { if (matched.length >= MAX_EMOTICON_SUGGESTIONS) {
break; break;
@@ -77,11 +76,11 @@ export default class EmoticonProvider {
// sort the emoticons so that emoticons starting with the entered text come first // sort the emoticons so that emoticons starting with the entered text come first
matched.sort((a, b) => { matched.sort((a, b) => {
const aPrefix = a.alias.startsWith(partialName); const aPrefix = a.name.startsWith(partialName);
const bPrefix = b.alias.startsWith(partialName); const bPrefix = b.name.startsWith(partialName);
if (aPrefix === bPrefix) { if (aPrefix === bPrefix) {
return a.alias.localeCompare(b.alias); return a.name.localeCompare(b.name);
} else if (aPrefix) { } else if (aPrefix) {
return -1; return -1;
} }
@@ -89,7 +88,7 @@ export default class EmoticonProvider {
return 1; return 1;
}); });
const terms = matched.map((emoticon) => ':' + emoticon.alias + ':'); const terms = matched.map((emoticon) => ':' + emoticon.name + ':');
if (terms.length > 0) { if (terms.length > 0) {
SuggestionStore.addSuggestions(suggestionId, terms, matched, EmoticonSuggestion, text); SuggestionStore.addSuggestions(suggestionId, terms, matched, EmoticonSuggestion, text);

Просмотреть файл

@@ -64,6 +64,20 @@
"add_command.username": "Response Username", "add_command.username": "Response Username",
"add_command.username.help": "Choose a username override for responses for this slash command. Usernames can consist of up to 22 characters consisting of lowercase letters, numbers and they symbols \"-\", \"_\", and \".\" .", "add_command.username.help": "Choose a username override for responses for this slash command. Usernames can consist of up to 22 characters consisting of lowercase letters, numbers and they symbols \"-\", \"_\", and \".\" .",
"add_command.username.placeholder": "Username", "add_command.username.placeholder": "Username",
"add_emoji.cancel": "Cancel",
"add_emoji.header": "Add",
"add_emoji.image": "Image",
"add_emoji.image.button": "Select",
"add_emoji.image.help": "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.",
"add_emoji.imageRequired": "An image is required for the emoji",
"add_emoji.name": "Name",
"add_emoji.name.help": "Choose a name for your emoji made of up to 64 characters consisting of lowercase letters, numbers, and the symbols '-' and '_'.",
"add_emoji.nameRequired": "A name is required for the emoji",
"add_emoji.nameInvalid": "An emoji's name can only contain numbers, letters, and the symbols '-' and '_'.",
"add_emoji.nameTaken": "This name is already in use by a system emoji. Please choose another name.",
"add_emoji.preview": "Preview",
"add_emoji.preview.sentence": "This is a sentence with {image} in it.",
"add_emoji.save": "Save",
"add_incoming_webhook.cancel": "Cancel", "add_incoming_webhook.cancel": "Cancel",
"add_incoming_webhook.channel": "Channel", "add_incoming_webhook.channel": "Channel",
"add_incoming_webhook.channelRequired": "A valid channel is required", "add_incoming_webhook.channelRequired": "A valid channel is required",
@@ -137,6 +151,7 @@
"admin.customization.enableCustomEmojiTitle": "Enable Custom Emoji:", "admin.customization.enableCustomEmojiTitle": "Enable Custom Emoji:",
"admin.customization.restrictCustomEmojiCreationAll": "Allow everyone to create custom emoji", "admin.customization.restrictCustomEmojiCreationAll": "Allow everyone to create custom emoji",
"admin.customization.restrictCustomEmojiCreationDesc": "Restrict the creation of custom emoji to certain users.", "admin.customization.restrictCustomEmojiCreationDesc": "Restrict the creation of custom emoji to certain users.",
"admin.customization.restrictCustomEmojiCreationAdmin": "Allow system and team admins to create custom emoji",
"admin.customization.restrictCustomEmojiCreationSystemAdmin": "Only allow system admins to create custom emoji", "admin.customization.restrictCustomEmojiCreationSystemAdmin": "Only allow system admins to create custom emoji",
"admin.customization.restrictCustomEmojiCreationTitle": "Restrict Custom Emoji Creation:", "admin.customization.restrictCustomEmojiCreationTitle": "Restrict Custom Emoji Creation:",
"admin.customization.support": "Legal and Support", "admin.customization.support": "Legal and Support",
@@ -701,6 +716,7 @@
"authorize.app": "The app <strong>{appName}</strong> would like the ability to access and modify your basic information.", "authorize.app": "The app <strong>{appName}</strong> would like the ability to access and modify your basic information.",
"authorize.deny": "Deny", "authorize.deny": "Deny",
"authorize.title": "An application would like to connect to your {teamName} account", "authorize.title": "An application would like to connect to your {teamName} account",
"backstage_list.search": "Search",
"backstage_navbar.backToMattermost": "Back to {siteName}", "backstage_navbar.backToMattermost": "Back to {siteName}",
"backstage_sidebar.integrations": "Integrations", "backstage_sidebar.integrations": "Integrations",
"backstage_sidebar.integrations.commands": "Slash Commands", "backstage_sidebar.integrations.commands": "Slash Commands",
@@ -858,6 +874,9 @@
"create_team.team_url.teamUrl": "Team URL", "create_team.team_url.teamUrl": "Team URL",
"create_team.team_url.unavailable": "This URL is unavailable. Please try another.", "create_team.team_url.unavailable": "This URL is unavailable. Please try another.",
"create_team.team_url.webAddress": "Choose the web address of your new team:", "create_team.team_url.webAddress": "Choose the web address of your new team:",
"custom_emoji.empty": "No custom emoji found",
"custom_emoji.header": "Custom Emoji",
"custom_emoji.search": "Search Custom Emoji",
"delete_channel.cancel": "Cancel", "delete_channel.cancel": "Cancel",
"delete_channel.channel": "channel", "delete_channel.channel": "channel",
"delete_channel.confirm": "Confirm DELETE Channel", "delete_channel.confirm": "Confirm DELETE Channel",
@@ -901,6 +920,15 @@
"email_verify.verified": "{siteName} Email Verified", "email_verify.verified": "{siteName} Email Verified",
"email_verify.verifiedBody": "<p>Your email has been verified! Click <a href={url}>here</a> to log in.</p>", "email_verify.verifiedBody": "<p>Your email has been verified! Click <a href={url}>here</a> to log in.</p>",
"email_verify.verifyFailed": "Failed to verify your email.", "email_verify.verifyFailed": "Failed to verify your email.",
"emoji_list.actions": "Actions",
"emoji_list.add": "Add Custom Emoji",
"emoji_list.creator": "Creator",
"emoji_list.delete": "Delete",
"emoji_list.empty": "No Custom Emoji Found",
"emoji_list.image": "Image",
"emoji_list.name": "Name",
"emoji_list.search": "Search Custom Emoji",
"emoji_list.somebody": "Somebody on another team",
"error.not_found.link_message": "Back to Mattermost", "error.not_found.link_message": "Back to Mattermost",
"error.not_found.message": "The page you were trying to reach does not exist", "error.not_found.message": "The page you were trying to reach does not exist",
"error.not_found.title": "Page not found", "error.not_found.title": "Page not found",
@@ -960,23 +988,25 @@
"installed_commands.add": "Add Slash Command", "installed_commands.add": "Add Slash Command",
"installed_commands.empty": "No commands found", "installed_commands.empty": "No commands found",
"installed_commands.header": "Slash Commands", "installed_commands.header": "Slash Commands",
"installed_commands.search": "Search Slash Commands",
"installed_commands.unnamed_command": "Unnamed Slash Command",
"installed_incoming_webhooks.add": "Add Incoming Webhook", "installed_incoming_webhooks.add": "Add Incoming Webhook",
"installed_incoming_webhooks.empty": "No incoming webhooks found", "installed_incoming_webhooks.empty": "No incoming webhooks found",
"installed_incoming_webhooks.header": "Incoming Webhooks", "installed_incoming_webhooks.header": "Incoming Webhooks",
"installed_incoming_webhooks.search": "Search Incoming Webhooks",
"installed_incoming_webhooks.unknown_channel": "A Private Webhook", "installed_incoming_webhooks.unknown_channel": "A Private Webhook",
"installed_integraions.unnamed_command": "Unnamed Slash Command",
"installed_integrations.callback_urls": "Callback URLs: {urls}", "installed_integrations.callback_urls": "Callback URLs: {urls}",
"installed_integrations.content_type": "Content-Type: {contentType}", "installed_integrations.content_type": "Content-Type: {contentType}",
"installed_integrations.creation": "Created by {creator} on {createAt, date, full}", "installed_integrations.creation": "Created by {creator} on {createAt, date, full}",
"installed_integrations.delete": "Delete", "installed_integrations.delete": "Delete",
"installed_integrations.regenToken": "Regenerate Token", "installed_integrations.regenToken": "Regenerate Token",
"installed_integrations.search": "Search Integrations",
"installed_integrations.token": "Token: {token}", "installed_integrations.token": "Token: {token}",
"installed_integrations.triggerWords": "Trigger Words: {triggerWords}", "installed_integrations.triggerWords": "Trigger Words: {triggerWords}",
"installed_integrations.url": "URL: {url}", "installed_integrations.url": "URL: {url}",
"installed_outgoing_webhooks.add": "Add Outgoing Webhook", "installed_outgoing_webhooks.add": "Add Outgoing Webhook",
"installed_outgoing_webhooks.empty": "No outgoing webhooks found", "installed_outgoing_webhooks.empty": "No outgoing webhooks found",
"installed_outgoing_webhooks.header": "Outgoing Webhooks", "installed_outgoing_webhooks.header": "Outgoing Webhooks",
"installed_outgoing_webhooks.search": "Search Outgoing Webhooks",
"installed_outgoing_webhooks.unknown_channel": "A Private Webhook", "installed_outgoing_webhooks.unknown_channel": "A Private Webhook",
"integrations.command.description": "Slash commands send events to external integrations", "integrations.command.description": "Slash commands send events to external integrations",
"integrations.command.title": "Slash Command", "integrations.command.title": "Slash Command",
@@ -1091,6 +1121,7 @@
"navbar_dropdown.accountSettings": "Account Settings", "navbar_dropdown.accountSettings": "Account Settings",
"navbar_dropdown.console": "System Console", "navbar_dropdown.console": "System Console",
"navbar_dropdown.create": "Create a New Team", "navbar_dropdown.create": "Create a New Team",
"navbar_dropdown.emoji": "Custom Emoji",
"navbar_dropdown.help": "Help", "navbar_dropdown.help": "Help",
"navbar_dropdown.integrations": "Integrations", "navbar_dropdown.integrations": "Integrations",
"navbar_dropdown.inviteMember": "Invite New Member", "navbar_dropdown.inviteMember": "Invite New Member",

Просмотреть файл

@@ -18,7 +18,7 @@
"keymirror": "0.1.1", "keymirror": "0.1.1",
"marked": "mattermost/marked#12d2be4cdf54d4ec95fead934e18840b6a2c1a7b", "marked": "mattermost/marked#12d2be4cdf54d4ec95fead934e18840b6a2c1a7b",
"match-at": "0.1.0", "match-at": "0.1.0",
"mattermost": "mattermost/mattermost-javascript#release-3.1", "mattermost": "mattermost/mattermost-javascript#8e4c320d5af653eacb248455d77057a76ec28830",
"object-assign": "4.1.0", "object-assign": "4.1.0",
"perfect-scrollbar": "0.6.11", "perfect-scrollbar": "0.6.11",
"react": "15.0.2", "react": "15.0.2",

24
webapp/routes/route_emoji.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,24 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import * as RouteUtils from 'routes/route_utils.jsx';
export default {
path: 'emoji',
getComponents: (location, callback) => {
System.import('components/backstage/backstage_controller.jsx').then(RouteUtils.importComponentSuccess(callback));
},
indexRoute: {
getComponents: (location, callback) => {
System.import('components/emoji/components/emoji_list.jsx').then(RouteUtils.importComponentSuccess(callback));
}
},
childRoutes: [
{
path: 'add',
getComponents: (location, callback) => {
System.import('components/emoji/components/add_emoji.jsx').then(RouteUtils.importComponentSuccess(callback));
}
}
]
};

Просмотреть файл

@@ -2,83 +2,65 @@
// See License.txt for license information. // See License.txt for license information.
import * as RouteUtils from 'routes/route_utils.jsx'; import * as RouteUtils from 'routes/route_utils.jsx';
import {Route, IndexRoute, Redirect} from 'react-router/es6';
import React from 'react';
import BackstageNavbar from 'components/backstage/backstage_navbar.jsx'; export default {
import BackstageSidebar from 'components/backstage/backstage_sidebar.jsx'; path: 'integrations',
import Integrations from 'components/backstage/integrations.jsx'; getComponents: (location, callback) => {
import InstalledIncomingWebhooks from 'components/backstage/installed_incoming_webhooks.jsx'; System.import('components/backstage/backstage_controller.jsx').then(RouteUtils.importComponentSuccess(callback));
import InstalledOutgoingWebhooks from 'components/backstage/installed_outgoing_webhooks.jsx'; },
import InstalledCommands from 'components/backstage/installed_commands.jsx'; indexRoute: {
import AddIncomingWebhook from 'components/backstage/add_incoming_webhook.jsx'; getComponents: (location, callback) => {
import AddOutgoingWebhook from 'components/backstage/add_outgoing_webhook.jsx'; System.import('components/integrations/components/integrations.jsx').then(RouteUtils.importComponentSuccess(callback));
import AddCommand from 'components/backstage/add_command.jsx'; }
},
export default ( childRoutes: [
<Route path='integrations'> {
<IndexRoute path: 'incoming_webhooks',
components={{ indexRoute: {
navbar: BackstageNavbar, getComponents: (location, callback) => {
sidebar: BackstageSidebar, System.import('components/integrations/components/installed_incoming_webhooks.jsx').then(RouteUtils.importComponentSuccess(callback));
center: Integrations }
}} },
/> childRoutes: [
<Route path='incoming_webhooks'> {
<IndexRoute path: 'add',
components={{ getComponents: (location, callback) => {
navbar: BackstageNavbar, System.import('components/integrations/components/add_incoming_webhook.jsx').then(RouteUtils.importComponentSuccess(callback));
sidebar: BackstageSidebar, }
center: InstalledIncomingWebhooks }
}} ]
/> },
<Route {
path='add' path: 'outgoing_webhooks',
components={{ indexRoute: {
navbar: BackstageNavbar, getComponents: (location, callback) => {
sidebar: BackstageSidebar, System.import('components/integrations/components/installed_outgoing_webhooks.jsx').then(RouteUtils.importComponentSuccess(callback));
center: AddIncomingWebhook }
}} },
/> childRoutes: [
</Route> {
<Route path='outgoing_webhooks'> path: 'add',
<IndexRoute getComponents: (location, callback) => {
components={{ System.import('components/integrations/components/add_outgoing_webhook.jsx').then(RouteUtils.importComponentSuccess(callback));
navbar: BackstageNavbar, }
sidebar: BackstageSidebar, }
center: InstalledOutgoingWebhooks ]
}} },
/> {
<Route path: 'commands',
path='add' indexRoute: {
components={{ getComponents: (location, callback) => {
navbar: BackstageNavbar, System.import('components/integrations/components/installed_commands.jsx').then(RouteUtils.importComponentSuccess(callback));
sidebar: BackstageSidebar, }
center: AddOutgoingWebhook },
}} childRoutes: [
/> {
</Route> path: 'add',
<Route path='commands'> getComponents: (location, callback) => {
<IndexRoute System.import('components/integrations/components/add_command.jsx').then(RouteUtils.importComponentSuccess(callback));
components={{ }
navbar: BackstageNavbar, }
sidebar: BackstageSidebar, ]
center: InstalledCommands }
}} ]
/> };
<Route
path='add'
components={{
navbar: BackstageNavbar,
sidebar: BackstageSidebar,
center: AddCommand
}}
/>
</Route>
<Redirect
from='*'
to='/error'
query={RouteUtils.notFoundParams}
/>
</Route>
);

Просмотреть файл

@@ -15,6 +15,9 @@ import Client from 'utils/web_client.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.jsx';
import ChannelStore from 'stores/channel_store.jsx'; import ChannelStore from 'stores/channel_store.jsx';
import emojiRoute from 'routes/route_emoji.jsx';
import integrationsRoute from 'routes/route_integrations.jsx';
function onChannelEnter(nextState, replace, callback) { function onChannelEnter(nextState, replace, callback) {
doChannelChange(nextState, replace, callback); doChannelChange(nextState, replace, callback);
} }
@@ -120,52 +123,52 @@ function onPermalinkEnter(nextState) {
export default { export default {
path: ':team', path: ':team',
getComponents: (location, callback) => {
System.import('components/needs_team.jsx').then(RouteUtils.importComponentSuccess(callback));
},
onEnter: preNeedsTeam, onEnter: preNeedsTeam,
indexRoute: {onEnter: (nextState, replace) => replace('/' + nextState.params.team + '/channels/town-square')}, indexRoute: {onEnter: (nextState, replace) => replace('/' + nextState.params.team + '/channels/town-square')},
childRoutes: [ childRoutes: [
integrationsRoute,
emojiRoute,
{ {
path: 'channels/:channel',
onEnter: onChannelEnter,
getComponents: (location, callback) => { getComponents: (location, callback) => {
Promise.all([ System.import('components/needs_team.jsx').then(RouteUtils.importComponentSuccess(callback));
System.import('components/sidebar.jsx'), },
System.import('components/channel_view.jsx') childRoutes: [
]).then( {
(comarr) => callback(null, {sidebar: comarr[0].default, center: comarr[1].default}) path: 'channels/:channel',
); onEnter: onChannelEnter,
} getComponents: (location, callback) => {
}, Promise.all([
{ System.import('components/sidebar.jsx'),
path: 'pl/:postid', System.import('components/channel_view.jsx')
onEnter: onPermalinkEnter, ]).then(
getComponents: (location, callback) => { (comarr) => callback(null, {sidebar: comarr[0].default, center: comarr[1].default})
Promise.all([ );
System.import('components/sidebar.jsx'), }
System.import('components/permalink_view.jsx') },
]).then( {
(comarr) => callback(null, {sidebar: comarr[0].default, center: comarr[1].default}) path: 'pl/:postid',
); onEnter: onPermalinkEnter,
} getComponents: (location, callback) => {
}, Promise.all([
{ System.import('components/sidebar.jsx'),
path: 'tutorial', System.import('components/permalink_view.jsx')
getComponents: (location, callback) => { ]).then(
Promise.all([ (comarr) => callback(null, {sidebar: comarr[0].default, center: comarr[1].default})
System.import('components/sidebar.jsx'), );
System.import('components/tutorial/tutorial_view.jsx') }
]).then( },
(comarr) => callback(null, {sidebar: comarr[0].default, center: comarr[1].default}) {
); path: 'tutorial',
} getComponents: (location, callback) => {
}, Promise.all([
{ System.import('components/sidebar.jsx'),
path: 'settings', System.import('components/tutorial/tutorial_view.jsx')
getChildRoutes: (location, callback) => { ]).then(
System.import('routes/route_integrations.jsx').then((comp) => callback(null, [comp.default])); (comarr) => callback(null, {sidebar: comarr[0].default, center: comarr[1].default})
} );
}
}
]
} }
] ]
}; };

Просмотреть файл

@@ -19,7 +19,6 @@
display: block; display: block;
.backstage-filter__search { .backstage-filter__search {
border-bottom: 1px solid $light-gray;
margin: 10px 0; margin: 10px 0;
width: 100%; width: 100%;
} }

Просмотреть файл

@@ -1,35 +1,11 @@
body { .backstage {
&.backstage { height: 100vh;
height: auto; width: 100vw;
overflow: auto;
.inner-wrap {
@include translateX(0);
margin-right: 0 !important;
&:before {
display: none;
}
}
.sidebar--right,
.sidebar--menu,
.navbar {
display: none;
}
}
}
.backstage-content {
background-color: $bg--gray;
height: 100%;
margin: 46px auto;
max-width: 960px;
padding-left: 135px;
} }
.backstage-navbar { .backstage-navbar {
background: $white; background: $white;
height: 41px;
border-bottom: 1px solid $light-gray; border-bottom: 1px solid $light-gray;
padding: 10px 20px; padding: 10px 20px;
z-index: 10; z-index: 10;
@@ -51,13 +27,29 @@ body {
} }
} }
.backstage-body {
background-color: $bg--gray;
bottom: 0;
display: inline-block;
height: calc(100vh - 41px);
overflow: auto;
top: 0;
width: 100%;
}
.backstage-content {
background-color: $bg--gray;
margin: 46px auto;
max-width: 960px;
padding-left: 135px;
vertical-align: top;
}
.backstage-sidebar { .backstage-sidebar {
height: 100%; padding: 46px 20px;
left: 0;
padding: 50px 20px;
position: absolute; position: absolute;
vertical-align: top;
width: 260px; width: 260px;
z-index: 5;
ul { ul {
list-style: none; list-style: none;
@@ -69,6 +61,7 @@ body {
border: 1px solid $light-gray; border: 1px solid $light-gray;
.category-title { .category-title {
color: $black;
display: block; display: block;
line-height: 36px; line-height: 36px;
padding: 0 10px; padding: 0 10px;
@@ -76,7 +69,8 @@ body {
} }
.category-title--active { .category-title--active {
color: $black; background-color: $primary-color;
color: $white;
} }
.category-title__text { .category-title__text {
@@ -110,8 +104,8 @@ body {
} }
} }
.backstage__sidebar__category + .backstage__sidebar__category { .backstage-sidebar__category + .backstage-sidebar__category {
border-top-width: 0; margin-top: 1em;
} }
.backstage-header__divider { .backstage-header__divider {
@@ -130,7 +124,7 @@ body {
margin: 5px 0; margin: 5px 0;
} }
.add-integrations-link { .add-link {
float: right; float: right;
} }
} }
@@ -139,45 +133,45 @@ body {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
width: 100%; width: 100%;
}
.backstage-filters__sort { .backstage-filters__sort {
flex-grow: 1; flex-grow: 1;
flex-shrink: 0; flex-shrink: 0;
line-height: 30px; line-height: 30px;
.filter-sort { .filter-sort {
text-decoration: none; text-decoration: none;
&.filter-sort--active { &.filter-sort--active {
color: inherit; color: inherit;
cursor: default; cursor: default;
}
}
.divider {
margin-left: 8px;
margin-right: 8px;
} }
} }
.backstage-filter__search { .divider {
flex-grow: 0; margin-left: 8px;
flex-shrink: 0; margin-right: 8px;
position: relative; }
width: 270px; }
.fa { .backstage-filter__search {
@include opacity(.4); flex-grow: 0;
left: 11px; flex-shrink: 0;
position: absolute; position: relative;
top: 11px; width: 270px;
}
input { .fa {
background: $white; @include opacity(.4);
border-bottom: none; left: 11px;
padding-left: 30px; position: absolute;
} top: 11px;
}
input {
background: $white;
border-bottom-width: 0;
padding-left: 30px;
} }
} }
@@ -332,3 +326,73 @@ body {
.integration-option__description { .integration-option__description {
color: $dark-gray; color: $dark-gray;
} }
.emoji-list .backstage-filter__search input {
border-bottom-width: 1px;
}
.emoji-list__help {
display: block;
padding: 1em 0;
}
.emoji-list__table {
width: 100%;
.backstage-list__item {
display: table-row;
}
.backstage-list__empty td {
padding: 15px 20px;
}
}
.emoji-list__table-header {
font-weight: bold;
}
.emoji-list__name {
padding: 20px 0px 20px 15px;
width: 30%;
}
.emoji-list__image {
padding: 15px 0px;
width: 15%;
}
&.emoji-list__creator {
padding: 15px 0px;
width: 40%;
}
&.emoji-list__actions {
padding: 20px 15px 20px 0px;
width: 15%;
}
.add-emoji__upload {
display: inline-block;
margin: 0 10px 10px 0;
position: relative;
input {
@include opacity(0);
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
z-index: 5;
}
}
.add-emoji__filename,
.add-emoji__preview {
padding-top: 7px;
.emoticon {
margin-bottom: 0;
}
}

135
webapp/stores/emoji_store.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,135 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import Constants from 'utils/constants.jsx';
import EventEmitter from 'events';
import EmojiJson from 'utils/emoji.json';
const ActionTypes = Constants.ActionTypes;
const CHANGE_EVENT = 'changed';
class EmojiStore extends EventEmitter {
constructor() {
super();
this.dispatchToken = AppDispatcher.register(this.handleEventPayload.bind(this));
this.emojis = new Map(EmojiJson);
this.systemEmojis = new Map(EmojiJson);
this.unicodeEmojis = new Map();
for (const [, emoji] of this.systemEmojis) {
if (emoji.unicode) {
this.unicodeEmojis.set(emoji.unicode, emoji);
}
}
this.receivedCustomEmojis = false;
this.customEmojis = new Map();
}
addChangeListener(callback) {
this.on(CHANGE_EVENT, callback);
}
removeChangeListener(callback) {
this.removeListener(CHANGE_EVENT, callback);
}
emitChange() {
this.emit(CHANGE_EVENT);
}
hasReceivedCustomEmojis() {
return this.receivedCustomEmojis;
}
setCustomEmojis(customEmojis) {
this.customEmojis = new Map();
for (const emoji of customEmojis) {
this.addCustomEmoji(emoji);
}
// add custom emojis to the map first so that they can't override system ones
this.emojis = new Map([...this.customEmojis, ...this.systemEmojis]);
}
addCustomEmoji(emoji) {
this.customEmojis.set(emoji.name, emoji);
}
removeCustomEmoji(id) {
for (const [name, emoji] of this.customEmojis) {
if (emoji.id === id) {
this.customEmojis.delete(name);
break;
}
}
}
getSystemEmojis() {
return this.systemEmojis;
}
getCustomEmojiMap() {
return this.customEmojis;
}
getEmojis() {
return this.emojis;
}
has(name) {
return this.emojis.has(name);
}
get(name) {
// prioritize system emojis so that custom ones can't override them
return this.emojis.get(name);
}
hasUnicode(codepoint) {
return this.unicodeEmojis.has(codepoint);
}
getUnicode(codepoint) {
return this.unicodeEmojis.get(codepoint);
}
getEmojiImageUrl(emoji) {
if (emoji.id) {
// must match Client.getCustomEmojiImageUrl
return `/api/v3/emoji/${emoji.id}`;
}
const filename = emoji.unicode || emoji.filename || emoji.name;
return Constants.EMOJI_PATH + '/' + filename + '.png';
}
handleEventPayload(payload) {
const action = payload.action;
switch (action.type) {
case ActionTypes.RECEIVED_CUSTOM_EMOJIS:
this.setCustomEmojis(action.emojis);
this.receivedCustomEmojis = true;
this.emitChange();
break;
case ActionTypes.RECEIVED_CUSTOM_EMOJI:
this.addCustomEmoji(action.emoji);
this.emitChange();
break;
case ActionTypes.REMOVED_CUSTOM_EMOJI:
this.removeCustomEmoji(action.id);
this.emitChange();
break;
}
}
}
export default new EmojiStore();

Просмотреть файл

@@ -160,13 +160,16 @@ class TeamStoreClass extends EventEmitter {
} }
isTeamAdminForCurrentTeam() { isTeamAdminForCurrentTeam() {
return this.isTeamAdmin(UserStore.getCurrentId(), this.getCurrentId());
}
isTeamAdmin(userId, teamId) {
if (!Utils) { if (!Utils) {
Utils = require('utils/utils.jsx'); //eslint-disable-line global-require Utils = require('utils/utils.jsx'); //eslint-disable-line global-require
} }
const userId = UserStore.getCurrentId();
var teamMembers = this.getTeamMembers(); var teamMembers = this.getTeamMembers();
const teamMember = teamMembers.find((m) => m.user_id === userId && m.team_id === this.getCurrentId()); const teamMember = teamMembers.find((m) => m.user_id === userId && m.team_id === teamId);
if (teamMember) { if (teamMember) {
return Utils.isAdmin(teamMember.roles); return Utils.isAdmin(teamMember.roles);

Просмотреть файл

@@ -3,38 +3,41 @@
import assert from 'assert'; import assert from 'assert';
import EmojiStore from 'stores/emoji_store.jsx';
import * as Emoticons from 'utils/emoticons.jsx'; import * as Emoticons from 'utils/emoticons.jsx';
describe('Emoticons', function() { describe('Emoticons', function() {
this.timeout(100000); this.timeout(100000);
it('handleEmoticons', function(done) { it('handleEmoticons', function(done) {
const emojis = EmojiStore.getEmojis();
assert.equal( assert.equal(
Emoticons.handleEmoticons(':goat: :dash:', new Map()), Emoticons.handleEmoticons(':goat: :dash:', new Map(), emojis),
'MM_EMOTICON0 MM_EMOTICON1', 'MM_EMOTICON0 MM_EMOTICON1',
'should replace emoticons with tokens' 'should replace emoticons with tokens'
); );
assert.equal( assert.equal(
Emoticons.handleEmoticons(':goat::dash:', new Map()), Emoticons.handleEmoticons(':goat::dash:', new Map(), emojis),
'MM_EMOTICON0MM_EMOTICON1', 'MM_EMOTICON0MM_EMOTICON1',
'should replace emoticons not separated by whitespace' 'should replace emoticons not separated by whitespace'
); );
assert.equal( assert.equal(
Emoticons.handleEmoticons('/:goat:..:dash:)', new Map()), Emoticons.handleEmoticons('/:goat:..:dash:)', new Map(), emojis),
'/MM_EMOTICON0..MM_EMOTICON1)', '/MM_EMOTICON0..MM_EMOTICON1)',
'should replace emoticons separated by punctuation' 'should replace emoticons separated by punctuation'
); );
assert.equal( assert.equal(
Emoticons.handleEmoticons('asdf:goat:asdf:dash:asdf', new Map()), Emoticons.handleEmoticons('asdf:goat:asdf:dash:asdf', new Map(), emojis),
'asdfMM_EMOTICON0asdfMM_EMOTICON1asdf', 'asdfMM_EMOTICON0asdfMM_EMOTICON1asdf',
'should replace emoticons separated by text' 'should replace emoticons separated by text'
); );
assert.equal( assert.equal(
Emoticons.handleEmoticons(':asdf: :goat : : dash:', new Map()), Emoticons.handleEmoticons(':asdf: :goat : : dash:', new Map(), emojis),
':asdf: :goat : : dash:', ':asdf: :goat : : dash:',
'shouldn\'t replace invalid emoticons' 'shouldn\'t replace invalid emoticons'
); );

Просмотреть файл

@@ -1384,3 +1384,88 @@ export function getPublicLink(filename, success, error) {
} }
); );
} }
export function listEmoji() {
if (isCallInProgress('listEmoji')) {
return;
}
callTracker.listEmoji = utils.getTimestamp();
Client.listEmoji(
(data) => {
callTracker.listEmoji = 0;
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_CUSTOM_EMOJIS,
emojis: data
});
},
(err) => {
callTracker.listEmoji = 0;
dispatchError(err, 'listEmoji');
}
);
}
export function addEmoji(emoji, image, success, error) {
const callName = 'addEmoji' + emoji.name;
if (isCallInProgress(callName)) {
return;
}
callTracker[callName] = utils.getTimestamp();
Client.addEmoji(
emoji,
image,
(data) => {
callTracker[callName] = 0;
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_CUSTOM_EMOJI,
emoji: data
});
if (success) {
success();
}
},
(err) => {
callTracker[callName] = 0;
if (error) {
error(err);
} else {
dispatchError(err, 'addEmoji');
}
}
);
}
export function deleteEmoji(id) {
const callName = 'deleteEmoji' + id;
if (isCallInProgress(callName)) {
return;
}
callTracker[callName] = utils.getTimestamp();
Client.deleteEmoji(
id,
() => {
callTracker[callName] = 0;
AppDispatcher.handleServerAction({
type: ActionTypes.REMOVED_CUSTOM_EMOJI,
id
});
},
(err) => {
callTracker[callName] = 0;
dispatchError(err, 'deleteEmoji');
}
);
}

Просмотреть файл

@@ -86,6 +86,11 @@ export default {
UPDATED_COMMAND: null, UPDATED_COMMAND: null,
REMOVED_COMMAND: null, REMOVED_COMMAND: null,
RECEIVED_CUSTOM_EMOJIS: null,
RECEIVED_CUSTOM_EMOJI: null,
UPDATED_CUSTOM_EMOJI: null,
REMOVED_CUSTOM_EMOJI: null,
RECEIVED_MSG: null, RECEIVED_MSG: null,
RECEIVED_MY_TEAM: null, RECEIVED_MY_TEAM: null,

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@@ -1,8 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
import Constants from './constants.jsx'; import EmojiStore from 'stores/emoji_store.jsx';
import emojis from './emoji.json';
export const emoticonPatterns = { export const emoticonPatterns = {
slightly_smiling_face: /(^|\s)(:-?\))(?=$|\s)/g, // :) slightly_smiling_face: /(^|\s)(:-?\))(?=$|\s)/g, // :)
@@ -27,117 +26,17 @@ export const emoticonPatterns = {
thumbsdown: /(^|\s)(:\-1:)(?=$|\s)/g // :-1: thumbsdown: /(^|\s)(:\-1:)(?=$|\s)/g // :-1:
}; };
let emoticonsByName; export function handleEmoticons(text, tokens, emojis) {
let emoticonsByCodePoint;
function initializeEmoticons() {
emoticonsByName = new Map();
emoticonsByCodePoint = new Set();
for (const emoji of emojis) {
const unicode = emoji.emoji;
let filename = '';
if (unicode) {
// this is a unicode emoji so the character code determines the file name
let codepoint = '';
for (let i = 0; i < unicode.length; i += 2) {
const code = fixedCharCodeAt(unicode, i);
// ignore variation selector characters
if (code >= 0xfe00 && code <= 0xfe0f) {
continue;
}
// some emoji (such as country flags) span multiple unicode characters
if (i !== 0) {
codepoint += '-';
}
codepoint += pad(code.toString(16));
}
filename = codepoint;
emoticonsByCodePoint.add(codepoint);
} else {
// this isn't a unicode emoji so the first alias determines the file name
filename = emoji.aliases[0];
}
for (const alias of emoji.aliases) {
emoticonsByName.set(alias, {
alias,
path: getImagePathForEmoticon(filename)
});
}
}
}
// Pads a hexadecimal number with zeroes to be at least 4 digits long
function pad(n) {
if (n.length >= 4) {
return n;
}
// http://stackoverflow.com/questions/10073699/pad-a-number-with-leading-zeros-in-javascript
return ('0000' + n).slice(-4);
}
// Gets the unicode character code of a character starting at the given index in the string
// Adapted from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt
function fixedCharCodeAt(str, idx = 0) {
// ex. fixedCharCodeAt('\uD800\uDC00', 0); // 65536
// ex. fixedCharCodeAt('\uD800\uDC00', 1); // false
const code = str.charCodeAt(idx);
// High surrogate (could change last hex to 0xDB7F to treat high
// private surrogates as single characters)
if (code >= 0xD800 && code <= 0xDBFF) {
const hi = code;
const low = str.charCodeAt(idx + 1);
if (isNaN(low)) {
console.log('High surrogate not followed by low surrogate in fixedCharCodeAt()'); // eslint-disable-line
}
return ((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
}
if (code >= 0xDC00 && code <= 0xDFFF) { // Low surrogate
// We return false to allow loops to skip this iteration since should have
// already handled high surrogate above in the previous iteration
return false;
}
return code;
}
export function getEmoticonsByName() {
if (!emoticonsByName) {
initializeEmoticons();
}
return emoticonsByName;
}
export function getEmoticonsByCodePoint() {
if (!emoticonsByCodePoint) {
initializeEmoticons();
}
return emoticonsByCodePoint;
}
export function handleEmoticons(text, tokens) {
let output = text; let output = text;
function replaceEmoticonWithToken(fullMatch, prefix, matchText, name) { function replaceEmoticonWithToken(fullMatch, prefix, matchText, name) {
if (getEmoticonsByName().has(name)) { const index = tokens.size;
const index = tokens.size; const alias = `MM_EMOTICON${index}`;
const alias = `MM_EMOTICON${index}`;
const path = getEmoticonsByName().get(name).path;
if (emojis.has(name)) {
const path = EmojiStore.getEmojiImageUrl(emojis.get(name));
// we have an image path so we found a matching emoticon
tokens.set(alias, { tokens.set(alias, {
value: `<img align="absmiddle" alt="${matchText}" class="emoticon" src="${path}" title="${matchText}" />`, value: `<img align="absmiddle" alt="${matchText}" class="emoticon" src="${path}" title="${matchText}" />`,
originalText: fullMatch originalText: fullMatch
@@ -163,7 +62,3 @@ export function handleEmoticons(text, tokens) {
return output; return output;
} }
export function getImagePathForEmoticon(name) {
return Constants.EMOJI_PATH + '/' + name + '.png';
}

Просмотреть файл

@@ -4,6 +4,7 @@
import Autolinker from 'autolinker'; import Autolinker from 'autolinker';
import {browserHistory} from 'react-router/es6'; import {browserHistory} from 'react-router/es6';
import Constants from './constants.jsx'; import Constants from './constants.jsx';
import EmojiStore from 'stores/emoji_store.jsx';
import * as Emoticons from './emoticons.jsx'; import * as Emoticons from './emoticons.jsx';
import * as Markdown from './markdown.jsx'; import * as Markdown from './markdown.jsx';
import PreferenceStore from 'stores/preference_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx';
@@ -61,7 +62,7 @@ export function doFormatText(text, options) {
output = autolinkHashtags(output, tokens); output = autolinkHashtags(output, tokens);
if (!('emoticons' in options) || options.emoticon) { if (!('emoticons' in options) || options.emoticon) {
output = Emoticons.handleEmoticons(output, tokens); output = Emoticons.handleEmoticons(output, tokens, options.emojis || EmojiStore.getEmojis());
} }
if (options.searchTerm) { if (options.searchTerm) {
@@ -75,15 +76,13 @@ export function doFormatText(text, options) {
if (!('emoticons' in options) || options.emoticon) { if (!('emoticons' in options) || options.emoticon) {
output = twemoji.parse(output, { output = twemoji.parse(output, {
className: 'emoticon', className: 'emoticon',
base: '', callback: (icon) => {
folder: Constants.EMOJI_PATH, if (!EmojiStore.hasUnicode(icon)) {
callback: (icon, twemojiOptions) => {
if (!Emoticons.getEmoticonsByCodePoint().has(icon)) {
// just leave the unicode characters and hope the browser can handle it // just leave the unicode characters and hope the browser can handle it
return null; return null;
} }
return ''.concat(twemojiOptions.base, twemojiOptions.size, '/', icon, twemojiOptions.ext); return EmojiStore.getEmojiImageUrl(EmojiStore.getUnicode(icon));
} }
}); });
} }

Просмотреть файл

@@ -982,7 +982,10 @@ export function getDisplayName(user) {
} }
export function displayUsername(userId) { export function displayUsername(userId) {
const user = UserStore.getProfile(userId); return displayUsernameForUser(UserStore.getProfile(userId));
}
export function displayUsernameForUser(user) {
const nameFormat = PreferenceStore.get(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'); const nameFormat = PreferenceStore.get(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false');
let username = ''; let username = '';