362 lines
12 KiB
JavaScript
362 lines
12 KiB
JavaScript
|
import React from 'react';
|
||
|
import Flux from '../../lib/flux';
|
||
|
import i18n from '../../i18n';
|
||
|
import AvatarUtils from '../../utils/AvatarUtils';
|
||
|
import Alert from '../Alert';
|
||
|
import Platforms from '../../lib/Platforms';
|
||
|
import Tooltip from '../common/Tooltip';
|
||
|
import {FormSection, FormTitle, FormTitleTags, FormText, FormTextTypes, FormDivider} from '../../uikit/form';
|
||
|
import Flex from '../../uikit/Flex';
|
||
|
import Spinner from '../../uikit/Spinner';
|
||
|
import Select from '../../uikit/Select';
|
||
|
import Integration from '../../uikit/Integration';
|
||
|
import EmptyState, {EmptyStateText, EmptyStateImage} from '../../uikit/EmptyState';
|
||
|
import Switch from '../../uikit/Switch';
|
||
|
import moment from 'moment';
|
||
|
import UserSettingsStore from '../../stores/UserSettingsStore';
|
||
|
import EmojiStore from '../../stores/EmojiStore';
|
||
|
import GuildSettingsStore from '../../stores/GuildSettingsStore';
|
||
|
import ModalActionCreators from '../../actions/ModalActionCreators';
|
||
|
import UserSettingsModalActionCreators from '../../actions/UserSettingsModalActionCreators';
|
||
|
import GuildSettingsActionCreators from '../../actions/GuildSettingsActionCreators';
|
||
|
import {GuildSettingsSections, UserSettingsSections} from '../../Constants';
|
||
|
import '../../styles/guild_settings_integrations.styl';
|
||
|
|
||
|
const SUB_EXPIRE_BEHAVIOR_OPTIONS = [
|
||
|
{value: '0', label: i18n.Messages.REMOVE_SYNCED_ROLE},
|
||
|
{value: '1', label: i18n.Messages.KICK_FROM_SERVER},
|
||
|
];
|
||
|
|
||
|
const SUB_GRACE_PERIOD_OPTIONS = [1, 3, 7, 14, 30].map(days => {
|
||
|
return {
|
||
|
value: `${days}`,
|
||
|
label: i18n.Messages.N_DAYS.format({days}),
|
||
|
};
|
||
|
});
|
||
|
|
||
|
class IntegrationItem extends React.PureComponent {
|
||
|
constructor(props) {
|
||
|
super(props);
|
||
|
|
||
|
(this: any).handleToggleEnabled = this.handleToggleEnabled.bind(this);
|
||
|
(this: any).handleExpireBehaviorChange = this.handleExpireBehaviorChange.bind(this);
|
||
|
(this: any).handleExpireGracePeriodChange = this.handleExpireGracePeriodChange.bind(this);
|
||
|
(this: any).handleSync = this.handleSync.bind(this);
|
||
|
(this: any).handleShowRole = this.handleShowRole.bind(this);
|
||
|
(this: any).handleToggleEmotes = this.handleToggleEmotes.bind(this);
|
||
|
}
|
||
|
|
||
|
renderTwitchEmoticons(platform) {
|
||
|
const {guild, integration} = this.props;
|
||
|
|
||
|
let syncedEmoji = null;
|
||
|
const emojis = EmojiStore.getGuildEmoji(guild.id);
|
||
|
if (integration['enable_emoticons'] && emojis) {
|
||
|
const emoticons = Object.values(emojis)
|
||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||
|
.filter(emoji => !emoji.roles.length || emoji.roles.indexOf(integration['role_id']) >= 0)
|
||
|
.map((emoji, index) => {
|
||
|
return (
|
||
|
<Tooltip key={index} text={emoji.name}>
|
||
|
<img
|
||
|
key={emoji.name}
|
||
|
draggable={false}
|
||
|
className="emoji jumboable"
|
||
|
src={AvatarUtils.getEmojiURL({id: emoji.id})}
|
||
|
/>
|
||
|
</Tooltip>
|
||
|
);
|
||
|
});
|
||
|
|
||
|
if (emoticons.length > 0) {
|
||
|
syncedEmoji = <div className="twitch-emojis margin-top-8">{emoticons}</div>;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return (
|
||
|
<div>
|
||
|
<FormDivider className="margin-top-20 margin-bottom-20" />
|
||
|
<Flex className="emoji-sync margin-bottom-8">
|
||
|
<Flex align={Flex.Align.CENTER}>
|
||
|
<FormText>{i18n.Messages.ENABLE_TWITCH_EMOJI_SYNC}</FormText>
|
||
|
</Flex>
|
||
|
<Flex.Child grow={0} shrink={0}>
|
||
|
<Switch
|
||
|
value={integration['enable_emoticons']}
|
||
|
onChange={this.handleToggleEmotes}
|
||
|
disabled={integration.syncing}
|
||
|
fill={platform.color}
|
||
|
/>
|
||
|
</Flex.Child>
|
||
|
</Flex>
|
||
|
{syncedEmoji}
|
||
|
</div>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
renderSyncDetails(formLabelSyncedSubs, numSubscribers) {
|
||
|
const {guild, integration} = this.props;
|
||
|
const role = guild.getRole(integration['role_id']);
|
||
|
|
||
|
let roleLink;
|
||
|
if (role != null) {
|
||
|
roleLink = <a className="synced-role-link" onClick={this.handleShowRole}>{role.name}</a>;
|
||
|
} else {
|
||
|
roleLink = i18n.Messages.NONE;
|
||
|
}
|
||
|
|
||
|
return (
|
||
|
<Flex>
|
||
|
<Flex.Child basis="50%">
|
||
|
<FormTitle className="margin-bottom-8">
|
||
|
{i18n.Messages.FORM_LABEL_SYNCED_ROLE}
|
||
|
</FormTitle>
|
||
|
<FormText>{roleLink}</FormText>
|
||
|
</Flex.Child>
|
||
|
<Flex.Child basis="50%">
|
||
|
<div className="sync-wrapper">
|
||
|
<FormTitle className="margin-bottom-8">
|
||
|
{formLabelSyncedSubs}
|
||
|
</FormTitle>
|
||
|
<Tooltip text={i18n.Messages.FORCE_SYNC}>
|
||
|
<button className="force-sync" disabled={integration.syncing} onClick={this.handleSync} />
|
||
|
</Tooltip>
|
||
|
</div>
|
||
|
<FormText>
|
||
|
{numSubscribers.format({subscribers: integration['subscriber_count']})}
|
||
|
</FormText>
|
||
|
<FormText type={FormTextTypes.DESCRIPTION}>
|
||
|
{i18n.Messages.LAST_SYNC.format({datetime: moment(integration['synced_at']).calendar()})}
|
||
|
</FormText>
|
||
|
</Flex.Child>
|
||
|
</Flex>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
renderExpireToggles(formLabelExpireBehavior) {
|
||
|
const {integration} = this.props;
|
||
|
|
||
|
return (
|
||
|
<Flex>
|
||
|
<Flex.Child basis="50%">
|
||
|
<FormTitle className="margin-bottom-8">{formLabelExpireBehavior}</FormTitle>
|
||
|
<Select
|
||
|
transparent
|
||
|
value={`${integration['expire_behavior']}`}
|
||
|
clearable={false}
|
||
|
searchable={false}
|
||
|
options={SUB_EXPIRE_BEHAVIOR_OPTIONS}
|
||
|
disabled={integration.syncing}
|
||
|
onChange={this.handleExpireBehaviorChange}
|
||
|
/>
|
||
|
</Flex.Child>
|
||
|
<Flex.Child basis="50%">
|
||
|
<FormTitle className="margin-bottom-8">
|
||
|
{i18n.Messages.FORM_LABEL_EXPIRE_GRACE_PERIOD}
|
||
|
</FormTitle>
|
||
|
<Select
|
||
|
transparent
|
||
|
value={`${integration['expire_grace_period']}`}
|
||
|
clearable={false}
|
||
|
searchable={false}
|
||
|
options={SUB_GRACE_PERIOD_OPTIONS}
|
||
|
disabled={integration.syncing}
|
||
|
onChange={this.handleExpireGracePeriodChange}
|
||
|
/>
|
||
|
</Flex.Child>
|
||
|
</Flex>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
render() {
|
||
|
const {integration, theme} = this.props;
|
||
|
const {enabled, syncing} = integration;
|
||
|
const platform = Platforms.get(integration.type);
|
||
|
|
||
|
let channelName;
|
||
|
let channelURL;
|
||
|
let formLabelExpireBehavior;
|
||
|
let formLabelSyncedSubs;
|
||
|
let numSubscribers;
|
||
|
let syncEmoticons;
|
||
|
switch (integration.type) {
|
||
|
case 'youtube':
|
||
|
channelName = integration.account.name;
|
||
|
channelURL = `gaming.youtube.com/channel/${integration.account.id}`;
|
||
|
formLabelExpireBehavior = i18n.Messages.FORM_LABEL_SPONSOR_EXPIRE_BEHAVIOR;
|
||
|
formLabelSyncedSubs = i18n.Messages.FORM_LABEL_SYNCED_SPONSORS;
|
||
|
numSubscribers = i18n.Messages.NUM_SPONSORS;
|
||
|
syncEmoticons = null;
|
||
|
break;
|
||
|
case 'twitch':
|
||
|
default:
|
||
|
channelName = channelURL = `twitch.tv/${integration.name}`;
|
||
|
formLabelExpireBehavior = i18n.Messages.FORM_LABEL_SUB_EXPIRE_BEHAVIOR;
|
||
|
formLabelSyncedSubs = i18n.Messages.FORM_LABEL_SYNCED_SUBS;
|
||
|
numSubscribers = i18n.Messages.NUM_SUBSCRIBERS;
|
||
|
syncEmoticons = this.renderTwitchEmoticons(platform);
|
||
|
}
|
||
|
|
||
|
let options;
|
||
|
if (enabled) {
|
||
|
options = (
|
||
|
<div className="integration-body">
|
||
|
{this.renderSyncDetails(formLabelSyncedSubs, numSubscribers)}
|
||
|
<FormDivider className="margin-top-20 margin-bottom-20" />
|
||
|
{this.renderExpireToggles(formLabelExpireBehavior)}
|
||
|
{syncEmoticons}
|
||
|
</div>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return (
|
||
|
<Integration
|
||
|
name={integration.user.toString()}
|
||
|
label={channelName}
|
||
|
labelUrl={`//${channelURL}`}
|
||
|
color={platform.color}
|
||
|
icon={platform.icon.color}
|
||
|
enabled={enabled}
|
||
|
syncing={syncing}
|
||
|
theme={theme}
|
||
|
handleSyncToggle={this.handleToggleEnabled}>
|
||
|
{options}
|
||
|
</Integration>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// Handlers
|
||
|
|
||
|
handleToggleEnabled() {
|
||
|
const {guild, integration} = this.props;
|
||
|
|
||
|
if (integration.syncing) return;
|
||
|
|
||
|
if (!integration.enabled) {
|
||
|
GuildSettingsActionCreators.enableIntegration(guild.id, integration.type, integration.id);
|
||
|
} else {
|
||
|
ModalActionCreators.push(props => {
|
||
|
const confirmText = integration['expire_behavior'] === 0
|
||
|
? i18n.Messages.REMOVE_SYNCED_ROLE
|
||
|
: i18n.Messages.KICK_FROM_SERVER;
|
||
|
return (
|
||
|
<Alert
|
||
|
title={i18n.Messages.DISABLE_INTEGRATION_TITLE}
|
||
|
body={
|
||
|
integration.type === 'youtube'
|
||
|
? i18n.Messages.DISABLE_INTEGRATION_YOUTUBE_BODY
|
||
|
: i18n.Messages.DISABLE_INTEGRATION_TWITCH_BODY
|
||
|
}
|
||
|
confirmText={confirmText}
|
||
|
cancelText={i18n.Messages.CANCEL}
|
||
|
onConfirm={() => GuildSettingsActionCreators.disableIntegration(guild.id, integration.id)}
|
||
|
{...props}
|
||
|
/>
|
||
|
);
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
handleExpireBehaviorChange(expireBehavior) {
|
||
|
const {guild, integration} = this.props;
|
||
|
GuildSettingsActionCreators.updateIntegration(
|
||
|
guild.id,
|
||
|
integration.id,
|
||
|
parseInt(expireBehavior.value),
|
||
|
integration['expire_grace_period'],
|
||
|
integration['enable_emoticons']
|
||
|
);
|
||
|
}
|
||
|
|
||
|
handleExpireGracePeriodChange(expireGracePeriod) {
|
||
|
const {guild, integration} = this.props;
|
||
|
GuildSettingsActionCreators.updateIntegration(
|
||
|
guild.id,
|
||
|
integration.id,
|
||
|
integration['expire_behavior'],
|
||
|
parseInt(expireGracePeriod.value),
|
||
|
integration['enable_emoticons']
|
||
|
);
|
||
|
}
|
||
|
|
||
|
handleSync() {
|
||
|
const {guild, integration} = this.props;
|
||
|
GuildSettingsActionCreators.syncIntegration(guild.id, integration.id);
|
||
|
}
|
||
|
|
||
|
handleShowRole() {
|
||
|
const {integration} = this.props;
|
||
|
GuildSettingsActionCreators.setSection(GuildSettingsSections.ROLES);
|
||
|
GuildSettingsActionCreators.selectRole(integration['role_id']);
|
||
|
}
|
||
|
|
||
|
handleToggleEmotes(e) {
|
||
|
const {guild, integration} = this.props;
|
||
|
GuildSettingsActionCreators.updateIntegration(
|
||
|
guild.id,
|
||
|
integration.id,
|
||
|
integration['expire_behavior'],
|
||
|
integration['expire_grace_period'],
|
||
|
e.currentTarget.checked
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class GuildSettingsIntegrations extends React.PureComponent {
|
||
|
render() {
|
||
|
const {guild, integrations, theme} = this.props;
|
||
|
|
||
|
let content;
|
||
|
if (integrations == null) {
|
||
|
content = <Spinner className="margin-top-40" type={Spinner.Type.SPINNING_CIRCLE} />;
|
||
|
} else if (integrations.length === 0) {
|
||
|
content = (
|
||
|
<EmptyState theme={theme} className="margin-top-20">
|
||
|
<EmptyStateImage
|
||
|
darkSrc={require('../../images/empties/empty_server_settings_integrations_dark.svg')}
|
||
|
lightSrc={require('../../images/empties/empty_server_settings_integrations_light.svg')}
|
||
|
width={294}
|
||
|
height={192}
|
||
|
/>
|
||
|
<EmptyStateText note={i18n.Messages.NO_INTEGRATIONS.format({onConnect: this.handleConnect})}>
|
||
|
{i18n.Messages.NO_INTEGRATIONS_LABEL}
|
||
|
</EmptyStateText>
|
||
|
</EmptyState>
|
||
|
);
|
||
|
} else {
|
||
|
content = integrations.map((integration, i) =>
|
||
|
<IntegrationItem key={i} integration={integration} guild={guild} theme={theme} />
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return (
|
||
|
<FormSection className="guild-settings-integrations" tag={FormTitleTags.H2} title={i18n.Messages.INTEGRATIONS}>
|
||
|
<FormText type={FormTextTypes.DESCRIPTION}>
|
||
|
{i18n.Messages.INTEGRATIONS_PRO_TIP.format({streamkitURL: 'https://discordapp.com/streamkit'})}
|
||
|
</FormText>
|
||
|
<FormDivider className="margin-top-20 margin-bottom-20" />
|
||
|
{content}
|
||
|
</FormSection>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// Handlers
|
||
|
|
||
|
handleConnect() {
|
||
|
UserSettingsModalActionCreators.open(UserSettingsSections.CONNECTIONS);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export default Flux.connectStores([GuildSettingsStore, UserSettingsStore], () => {
|
||
|
const {guild, integrations} = GuildSettingsStore.getProps();
|
||
|
return {
|
||
|
guild,
|
||
|
integrations,
|
||
|
theme: UserSettingsStore.theme,
|
||
|
};
|
||
|
})(GuildSettingsIntegrations);
|
||
|
|
||
|
|
||
|
|
||
|
// WEBPACK FOOTER //
|
||
|
// ./discord_app/components/guild_settings/GuildSettingsIntegrations.js
|