mirror of https://git.sr.ht/~ashkeel/strimertul
refactor: move logic around!
This commit is contained in:
parent
07e3a00990
commit
3d0e824b4b
|
@ -1,3 +1,6 @@
|
|||
import { GetTwitchAuthURL } from '@wailsapp/go/main/App';
|
||||
import { BrowserOpenURL } from '@wailsapp/runtime/runtime';
|
||||
|
||||
export interface TwitchCredentials {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
|
@ -57,3 +60,12 @@ export async function checkTwitchKeys(
|
|||
throw new Error(`API test call failed: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the user's browser to authenticate with Twitch
|
||||
* @param target What's the target of the authentication (stream/chat account)
|
||||
*/
|
||||
export async function startAuthFlow(target: string) {
|
||||
const url = await GetTwitchAuthURL(target);
|
||||
BrowserOpenURL(url);
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ import LoyaltyRewardsPage from './pages/LoyaltyRewards';
|
|||
import OnboardingPage from './pages/Onboarding';
|
||||
import ServerSettingsPage from './pages/ServerSettings';
|
||||
import StrimertulPage from './pages/Strimertul';
|
||||
import TwitchSettingsPage from './pages/TwitchSettings';
|
||||
import TwitchSettingsPage from './pages/TwitchSettings/Page';
|
||||
import UISettingsPage from './pages/UISettingsPage';
|
||||
import ExtensionsPage from './pages/Extensions';
|
||||
import { getTheme, styled } from './theme';
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
import { GetTwitchLoggedUser } from '@wailsapp/go/main/App';
|
||||
import { helix } from '@wailsapp/go/models';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppSelector } from '~/store';
|
||||
import { TextBlock, styled } from '../theme';
|
||||
|
||||
interface SyncError {
|
||||
ok: false;
|
||||
error: string;
|
||||
}
|
||||
|
||||
const TwitchUser = styled('div', {
|
||||
display: 'flex',
|
||||
gap: '0.8rem',
|
||||
alignItems: 'center',
|
||||
fontSize: '14pt',
|
||||
fontWeight: '300',
|
||||
});
|
||||
const TwitchPic = styled('img', {
|
||||
width: '48px',
|
||||
borderRadius: '50%',
|
||||
});
|
||||
const TwitchName = styled('p', { fontWeight: 'bold' });
|
||||
|
||||
export default function TwitchUserBlock({ authKey }: { authKey: string }) {
|
||||
const { t } = useTranslation();
|
||||
const [user, setUser] = useState<helix.User | SyncError>(null);
|
||||
const kv = useAppSelector((state) => state.api.client);
|
||||
|
||||
const getUserInfo = async () => {
|
||||
try {
|
||||
const res = await GetTwitchLoggedUser(authKey);
|
||||
setUser(res);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setUser({ ok: false, error: (e as Error).message });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Get user info
|
||||
void getUserInfo();
|
||||
|
||||
const onKeyChange = () => {
|
||||
void getUserInfo();
|
||||
};
|
||||
void kv.subscribeKey(authKey, onKeyChange);
|
||||
return () => {
|
||||
void kv.unsubscribeKey(authKey, onKeyChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (user !== null) {
|
||||
if ('id' in user) {
|
||||
return (
|
||||
<TwitchUser>
|
||||
<TextBlock>
|
||||
{t('pages.twitch-settings.events.authenticated-as')}
|
||||
</TextBlock>
|
||||
<TwitchPic
|
||||
src={user.profile_image_url}
|
||||
alt={t('pages.twitch-settings.events.profile-picture')}
|
||||
/>
|
||||
<TwitchName>{user.display_name}</TwitchName>
|
||||
</TwitchUser>
|
||||
);
|
||||
}
|
||||
return <span>{t('pages.twitch-settings.events.err-no-user')}</span>;
|
||||
}
|
||||
|
||||
return <i>{t('pages.twitch-settings.events.loading-data')}</i>;
|
||||
}
|
|
@ -11,7 +11,11 @@ import { useEffect, useState } from 'react';
|
|||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useModule } from '~/lib/react';
|
||||
import { checkTwitchKeys, TwitchCredentials } from '~/lib/twitch';
|
||||
import {
|
||||
checkTwitchKeys,
|
||||
startAuthFlow,
|
||||
TwitchCredentials,
|
||||
} from '~/lib/twitch';
|
||||
import { languages } from '~/locale/languages';
|
||||
import { RootState, useAppDispatch } from '~/store';
|
||||
import apiReducer, { modules } from '~/store/api/reducer';
|
||||
|
@ -42,6 +46,7 @@ import {
|
|||
themes,
|
||||
} from '../theme';
|
||||
import { Alert } from '../theme/alert';
|
||||
import TwitchUserBlock from '../components/TwitchUserBlock';
|
||||
|
||||
const Container = styled('div', {
|
||||
display: 'flex',
|
||||
|
@ -428,56 +433,12 @@ function TwitchIntegrationStep() {
|
|||
);
|
||||
}
|
||||
|
||||
interface SyncError {
|
||||
ok: false;
|
||||
error: string;
|
||||
}
|
||||
|
||||
const TwitchUser = styled('div', {
|
||||
display: 'flex',
|
||||
gap: '0.8rem',
|
||||
alignItems: 'center',
|
||||
fontSize: '14pt',
|
||||
fontWeight: '300',
|
||||
});
|
||||
const TwitchPic = styled('img', {
|
||||
width: '48px',
|
||||
borderRadius: '50%',
|
||||
});
|
||||
const TwitchName = styled('p', { fontWeight: 'bold' });
|
||||
|
||||
function TwitchEventsStep() {
|
||||
const { t } = useTranslation();
|
||||
const [userStatus, setUserStatus] = useState<helix.User | SyncError>(null);
|
||||
const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig);
|
||||
const [uiConfig, setUiConfig] = useModule(modules.uiConfig);
|
||||
const kv = useSelector((state: RootState) => state.api.client);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const getUserInfo = async () => {
|
||||
try {
|
||||
const res = await GetTwitchLoggedUser('twitch/auth-keys');
|
||||
setUserStatus(res);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setUserStatus({ ok: false, error: (e as Error).message });
|
||||
}
|
||||
};
|
||||
|
||||
const startAuthFlow = async () => {
|
||||
const url = await GetTwitchAuthURL('stream');
|
||||
BrowserOpenURL(url);
|
||||
};
|
||||
|
||||
const finishStep = async () => {
|
||||
if ('id' in userStatus) {
|
||||
// Set bot config to sane defaults
|
||||
await dispatch(
|
||||
setTwitchConfig({
|
||||
...twitchConfig,
|
||||
}),
|
||||
);
|
||||
}
|
||||
await dispatch(
|
||||
setUiConfig({
|
||||
...uiConfig,
|
||||
|
@ -487,50 +448,6 @@ function TwitchEventsStep() {
|
|||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Get user info
|
||||
void getUserInfo();
|
||||
|
||||
const onKeyChange = () => {
|
||||
void getUserInfo();
|
||||
};
|
||||
|
||||
void kv.subscribeKey('twitch/auth-keys', onKeyChange);
|
||||
return () => {
|
||||
void kv.unsubscribeKey('twitch/auth-keys', onKeyChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
let userBlock = <i>{t('pages.twitch-settings.events.loading-data')}</i>;
|
||||
if (userStatus !== null) {
|
||||
if ('id' in userStatus) {
|
||||
userBlock = (
|
||||
<>
|
||||
<TwitchUser>
|
||||
<TextBlock>
|
||||
{t('pages.twitch-settings.events.authenticated-as')}
|
||||
</TextBlock>
|
||||
<TwitchPic
|
||||
src={userStatus.profile_image_url}
|
||||
alt={t('pages.twitch-settings.events.profile-picture')}
|
||||
/>
|
||||
<TwitchName>{userStatus.display_name}</TwitchName>
|
||||
</TwitchUser>
|
||||
<TextBlock>{t('pages.onboarding.twitch-ev-p3')}</TextBlock>
|
||||
<Button
|
||||
variation={'primary'}
|
||||
onClick={() => {
|
||||
void finishStep();
|
||||
}}
|
||||
>
|
||||
{t('pages.onboarding.twitch-complete')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
userBlock = <span>{t('pages.twitch-settings.events.err-no-user')}</span>;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<TextBlock>{t('pages.onboarding.twitch-ev-p1')}</TextBlock>
|
||||
|
@ -539,7 +456,7 @@ function TwitchEventsStep() {
|
|||
<Button
|
||||
variation="primary"
|
||||
onClick={() => {
|
||||
void startAuthFlow();
|
||||
void startAuthFlow('stream');
|
||||
}}
|
||||
>
|
||||
<ExternalLinkIcon /> {t('pages.twitch-settings.events.auth-button')}
|
||||
|
@ -548,7 +465,16 @@ function TwitchEventsStep() {
|
|||
<SectionHeader>
|
||||
{t('pages.twitch-settings.events.current-status')}
|
||||
</SectionHeader>
|
||||
{userBlock}
|
||||
<TwitchUserBlock authKey="twitch/auth-keys" />
|
||||
<TextBlock>{t('pages.onboarding.twitch-ev-p3')}</TextBlock>
|
||||
<Button
|
||||
variation={'primary'}
|
||||
onClick={() => {
|
||||
void finishStep();
|
||||
}}
|
||||
>
|
||||
{t('pages.onboarding.twitch-complete')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,441 +0,0 @@
|
|||
import { CheckIcon, ExternalLinkIcon } from '@radix-ui/react-icons';
|
||||
import { GetTwitchAuthURL, GetTwitchLoggedUser } from '@wailsapp/go/main/App';
|
||||
import { helix } from '@wailsapp/go/models';
|
||||
import { BrowserOpenURL } from '@wailsapp/runtime/runtime';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import eventsubTests from '~/data/eventsub-tests';
|
||||
import { useModule, useStatus } from '~/lib/react';
|
||||
import { useAppDispatch, useAppSelector } from '~/store';
|
||||
import apiReducer, { modules } from '~/store/api/reducer';
|
||||
import { checkTwitchKeys } from '~/lib/twitch';
|
||||
import BrowserLink from '../components/BrowserLink';
|
||||
import DefinitionTable from '../components/DefinitionTable';
|
||||
import RevealLink from '../components/utils/RevealLink';
|
||||
import SaveButton from '../components/forms/SaveButton';
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Checkbox,
|
||||
CheckboxIndicator,
|
||||
Field,
|
||||
FlexRow,
|
||||
InputBox,
|
||||
Label,
|
||||
PageContainer,
|
||||
PageHeader,
|
||||
PageTitle,
|
||||
PasswordInputBox,
|
||||
SectionHeader,
|
||||
styled,
|
||||
TabButton,
|
||||
TabContainer,
|
||||
TabContent,
|
||||
TabList,
|
||||
TextBlock,
|
||||
} from '../theme';
|
||||
import AlertContent from '../components/AlertContent';
|
||||
import { Alert } from '../theme/alert';
|
||||
|
||||
const StepList = styled('ul', {
|
||||
lineHeight: '1.5',
|
||||
listStyleType: 'none',
|
||||
listStylePosition: 'outside',
|
||||
});
|
||||
const Step = styled('li', {
|
||||
marginBottom: '0.5rem',
|
||||
paddingLeft: '1rem',
|
||||
'&::marker': {
|
||||
color: '$teal11',
|
||||
content: '▧',
|
||||
display: 'inline-block',
|
||||
marginLeft: '-0.5rem',
|
||||
},
|
||||
});
|
||||
|
||||
type TestResult = { open: boolean; error?: Error };
|
||||
|
||||
function TwitchAPISettings() {
|
||||
const { t } = useTranslation();
|
||||
const [httpConfig] = useModule(modules.httpConfig);
|
||||
const [twitchConfig, setTwitchConfig, loadStatus] = useModule(
|
||||
modules.twitchConfig,
|
||||
);
|
||||
const status = useStatus(loadStatus.save);
|
||||
const dispatch = useAppDispatch();
|
||||
const [revealClientSecret, setRevealClientSecret] = useState(false);
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [testResult, setTestResult] = useState<TestResult>({
|
||||
open: false,
|
||||
});
|
||||
|
||||
const checkCredentials = async () => {
|
||||
setTesting(true);
|
||||
if (twitchConfig) {
|
||||
try {
|
||||
await checkTwitchKeys(
|
||||
twitchConfig.api_client_id,
|
||||
twitchConfig.api_client_secret,
|
||||
);
|
||||
setTestResult({ open: true });
|
||||
} catch (e: unknown) {
|
||||
setTestResult({ open: true, error: e as Error });
|
||||
}
|
||||
}
|
||||
setTesting(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(ev) => {
|
||||
void dispatch(setTwitchConfig(twitchConfig));
|
||||
ev.preventDefault();
|
||||
}}
|
||||
>
|
||||
<SectionHeader spacing={'none'}>
|
||||
{t('pages.twitch-settings.api-subheader')}
|
||||
</SectionHeader>
|
||||
<TextBlock>{t('pages.twitch-settings.apiguide-1')}</TextBlock>
|
||||
<StepList>
|
||||
<Step>
|
||||
<Trans i18nKey="pages.twitch-settings.apiguide-2">
|
||||
{' '}
|
||||
<BrowserLink href="https://dev.twitch.tv/console/apps/create">
|
||||
https://dev.twitch.tv/console/apps/create
|
||||
</BrowserLink>
|
||||
</Trans>
|
||||
</Step>
|
||||
<Step>
|
||||
{t('pages.twitch-settings.apiguide-3')}
|
||||
|
||||
<DefinitionTable
|
||||
entries={{
|
||||
[t('pages.twitch-settings.app-oauth-redirect-url')]: `http://${
|
||||
httpConfig?.bind.indexOf(':') > 0
|
||||
? httpConfig.bind
|
||||
: `localhost${httpConfig?.bind ?? ':4337'}`
|
||||
}/twitch/callback`,
|
||||
[t('pages.twitch-settings.app-category')]: 'Broadcasting Suite',
|
||||
}}
|
||||
/>
|
||||
</Step>
|
||||
<Step>
|
||||
<Trans i18nKey="pages.twitch-settings.apiguide-4">
|
||||
{'str1 '}
|
||||
<b>str2</b>
|
||||
</Trans>
|
||||
</Step>
|
||||
</StepList>
|
||||
<Field size="fullWidth" css={{ marginTop: '2rem' }}>
|
||||
<Label htmlFor="clientid">
|
||||
{t('pages.twitch-settings.app-client-id')}
|
||||
</Label>
|
||||
<InputBox
|
||||
type="text"
|
||||
id="clientid"
|
||||
placeholder={t('pages.twitch-settings.app-client-id')}
|
||||
required={true}
|
||||
value={twitchConfig?.api_client_id ?? ''}
|
||||
onChange={(ev) =>
|
||||
dispatch(
|
||||
apiReducer.actions.twitchConfigChanged({
|
||||
...twitchConfig,
|
||||
api_client_id: ev.target.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field size="fullWidth">
|
||||
<Label htmlFor="clientsecret">
|
||||
{t('pages.twitch-settings.app-client-secret')}
|
||||
<RevealLink
|
||||
value={revealClientSecret}
|
||||
setter={setRevealClientSecret}
|
||||
/>
|
||||
</Label>
|
||||
<PasswordInputBox
|
||||
reveal={revealClientSecret}
|
||||
id="clientsecret"
|
||||
placeholder={t('pages.twitch-settings.app-client-secret')}
|
||||
required={true}
|
||||
value={twitchConfig?.api_client_secret ?? ''}
|
||||
onChange={(ev) =>
|
||||
dispatch(
|
||||
apiReducer.actions.twitchConfigChanged({
|
||||
...twitchConfig,
|
||||
api_client_secret: ev.target.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<ButtonGroup>
|
||||
<SaveButton status={status} />
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void checkCredentials();
|
||||
}}
|
||||
disabled={testing}
|
||||
>
|
||||
{t('pages.twitch-settings.test-button')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Alert
|
||||
defaultOpen={false}
|
||||
open={testResult.open}
|
||||
onOpenChange={(val: boolean) => {
|
||||
setTestResult({ ...testResult, open: val });
|
||||
}}
|
||||
>
|
||||
<AlertContent
|
||||
variation={testResult.error ? 'danger' : 'default'}
|
||||
description={
|
||||
testResult.error
|
||||
? t('pages.twitch-settings.test-failed', {
|
||||
error: testResult.error.message,
|
||||
})
|
||||
: t('pages.twitch-settings.test-succeeded')
|
||||
}
|
||||
actionText={t('form-actions.ok')}
|
||||
onAction={() => {
|
||||
setTestResult({ ...testResult, open: false });
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
interface SyncError {
|
||||
ok: false;
|
||||
error: string;
|
||||
}
|
||||
|
||||
const TwitchUser = styled('div', {
|
||||
display: 'flex',
|
||||
gap: '0.8rem',
|
||||
alignItems: 'center',
|
||||
fontSize: '14pt',
|
||||
fontWeight: '300',
|
||||
});
|
||||
const TwitchPic = styled('img', {
|
||||
width: '48px',
|
||||
borderRadius: '50%',
|
||||
});
|
||||
const TwitchName = styled('p', { fontWeight: 'bold' });
|
||||
|
||||
async function startAuthFlow(target: string) {
|
||||
const url = await GetTwitchAuthURL(target);
|
||||
BrowserOpenURL(url);
|
||||
}
|
||||
|
||||
function TwitchUserBlock({ authKey }: { authKey: string }) {
|
||||
const { t } = useTranslation();
|
||||
const [user, setUser] = useState<helix.User | SyncError>(null);
|
||||
const kv = useAppSelector((state) => state.api.client);
|
||||
|
||||
const getUserInfo = async () => {
|
||||
try {
|
||||
const res = await GetTwitchLoggedUser(authKey);
|
||||
setUser(res);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setUser({ ok: false, error: (e as Error).message });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Get user info
|
||||
void getUserInfo();
|
||||
|
||||
const onKeyChange = () => {
|
||||
void getUserInfo();
|
||||
};
|
||||
void kv.subscribeKey(authKey, onKeyChange);
|
||||
return () => {
|
||||
void kv.unsubscribeKey(authKey, onKeyChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (user !== null) {
|
||||
if ('id' in user) {
|
||||
return (
|
||||
<TwitchUser>
|
||||
<TextBlock>
|
||||
{t('pages.twitch-settings.events.authenticated-as')}
|
||||
</TextBlock>
|
||||
<TwitchPic
|
||||
src={user.profile_image_url}
|
||||
alt={t('pages.twitch-settings.events.profile-picture')}
|
||||
/>
|
||||
<TwitchName>{user.display_name}</TwitchName>
|
||||
</TwitchUser>
|
||||
);
|
||||
}
|
||||
return <span>{t('pages.twitch-settings.events.err-no-user')}</span>;
|
||||
}
|
||||
|
||||
return <i>{t('pages.twitch-settings.events.loading-data')}</i>;
|
||||
}
|
||||
|
||||
function TwitchEventSubSettings() {
|
||||
const { t } = useTranslation();
|
||||
const kv = useAppSelector((state) => state.api.client);
|
||||
|
||||
const sendFakeEvent = async (event: keyof typeof eventsubTests) => {
|
||||
const data = eventsubTests[event];
|
||||
await kv.putJSON(`twitch/ev/eventsub-event/${event}`, {
|
||||
...data,
|
||||
subscription: {
|
||||
...data.subscription,
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
date: new Date().toISOString(),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextBlock>{t('pages.twitch-settings.events.auth-message')}</TextBlock>
|
||||
<Button
|
||||
variation="primary"
|
||||
onClick={() => {
|
||||
void startAuthFlow('stream');
|
||||
}}
|
||||
>
|
||||
<ExternalLinkIcon /> {t('pages.twitch-settings.events.auth-button')}
|
||||
</Button>
|
||||
<SectionHeader>
|
||||
{t('pages.twitch-settings.events.current-status')}
|
||||
</SectionHeader>
|
||||
<TwitchUserBlock authKey={'twitch/auth-keys'} />
|
||||
<SectionHeader>
|
||||
{t('pages.twitch-settings.events.sim-events')}
|
||||
</SectionHeader>
|
||||
<ButtonGroup>
|
||||
{Object.keys(eventsubTests).map((ev: keyof typeof eventsubTests) => (
|
||||
<Button
|
||||
key={ev}
|
||||
onClick={() => {
|
||||
void sendFakeEvent(ev);
|
||||
}}
|
||||
>
|
||||
{t(`pages.twitch-settings.events.sim.${ev}`, { defaultValue: ev })}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TwitchChatSettings() {
|
||||
const [chatConfig, setChatConfig, loadStatus] = useModule(
|
||||
modules.twitchChatConfig,
|
||||
);
|
||||
const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig);
|
||||
const status = useStatus(loadStatus.save);
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const disabled = status?.type === 'pending';
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(ev) => {
|
||||
void dispatch(setTwitchConfig(twitchConfig));
|
||||
void dispatch(setChatConfig(chatConfig));
|
||||
ev.preventDefault();
|
||||
}}
|
||||
>
|
||||
<SectionHeader>
|
||||
{t('pages.twitch-settings.bot-chat-header')}
|
||||
</SectionHeader>
|
||||
<Field size="fullWidth">
|
||||
<Label htmlFor="bot-chat-history">
|
||||
{t('pages.twitch-settings.bot-chat-cooldown-tip')}
|
||||
</Label>
|
||||
<InputBox
|
||||
type="number"
|
||||
id="bot-chat-history"
|
||||
required={true}
|
||||
disabled={disabled}
|
||||
defaultValue={
|
||||
chatConfig ? chatConfig.command_cooldown ?? 2 : undefined
|
||||
}
|
||||
onChange={(ev) =>
|
||||
dispatch(
|
||||
apiReducer.actions.twitchChatConfigChanged({
|
||||
...chatConfig,
|
||||
command_cooldown: parseInt(ev.target.value, 10),
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<SaveButton status={status} />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TwitchSettingsPage(): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const active = twitchConfig?.enabled ?? false;
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<PageTitle>{t('pages.twitch-settings.title')}</PageTitle>
|
||||
<TextBlock>{t('pages.twitch-settings.subtitle')}</TextBlock>
|
||||
<Field css={{ paddingTop: '1rem' }}>
|
||||
<FlexRow spacing={1}>
|
||||
<Checkbox
|
||||
checked={active}
|
||||
onCheckedChange={(ev) => {
|
||||
void dispatch(
|
||||
setTwitchConfig({
|
||||
...twitchConfig,
|
||||
enabled: !!ev,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
id="enable"
|
||||
>
|
||||
<CheckboxIndicator>{active && <CheckIcon />}</CheckboxIndicator>
|
||||
</Checkbox>
|
||||
|
||||
<Label htmlFor="enable">{t('pages.twitch-settings.enable')}</Label>
|
||||
</FlexRow>
|
||||
</Field>
|
||||
</PageHeader>
|
||||
<div style={{ display: active ? '' : 'none' }}>
|
||||
<TabContainer defaultValue="api-config">
|
||||
<TabList>
|
||||
<TabButton value="api-config">
|
||||
{t('pages.twitch-settings.api-configuration')}
|
||||
</TabButton>
|
||||
<TabButton value="eventsub">
|
||||
{t('pages.twitch-settings.eventsub')}
|
||||
</TabButton>
|
||||
<TabButton value="chat-settings">
|
||||
{t('pages.twitch-settings.chat-settings')}
|
||||
</TabButton>
|
||||
</TabList>
|
||||
<TabContent value="api-config">
|
||||
<TwitchAPISettings />
|
||||
</TabContent>
|
||||
<TabContent value="eventsub">
|
||||
<TwitchEventSubSettings />
|
||||
</TabContent>
|
||||
<TabContent value="chat-settings">
|
||||
<TwitchChatSettings />
|
||||
</TabContent>
|
||||
</TabContainer>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import { CheckIcon } from '@radix-ui/react-icons';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useModule } from '~/lib/react';
|
||||
import { useAppDispatch } from '~/store';
|
||||
import { modules } from '~/store/api/reducer';
|
||||
import {
|
||||
Checkbox,
|
||||
CheckboxIndicator,
|
||||
Field,
|
||||
FlexRow,
|
||||
Label,
|
||||
PageContainer,
|
||||
PageHeader,
|
||||
PageTitle,
|
||||
TabButton,
|
||||
TabContainer,
|
||||
TabContent,
|
||||
TabList,
|
||||
TextBlock,
|
||||
} from '../../theme';
|
||||
import TwitchAPISettings from './TwitchAPISettings';
|
||||
import TwitchEventSubSettings from './TwitchEventSubSettings';
|
||||
import TwitchChatSettings from './TwitchChatSettings';
|
||||
|
||||
export default function TwitchSettingsPage(): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const active = twitchConfig?.enabled ?? false;
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<PageTitle>{t('pages.twitch-settings.title')}</PageTitle>
|
||||
<TextBlock>{t('pages.twitch-settings.subtitle')}</TextBlock>
|
||||
<Field css={{ paddingTop: '1rem' }}>
|
||||
<FlexRow spacing={1}>
|
||||
<Checkbox
|
||||
checked={active}
|
||||
onCheckedChange={(ev) => {
|
||||
void dispatch(
|
||||
setTwitchConfig({
|
||||
...twitchConfig,
|
||||
enabled: !!ev,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
id="enable"
|
||||
>
|
||||
<CheckboxIndicator>{active && <CheckIcon />}</CheckboxIndicator>
|
||||
</Checkbox>
|
||||
|
||||
<Label htmlFor="enable">{t('pages.twitch-settings.enable')}</Label>
|
||||
</FlexRow>
|
||||
</Field>
|
||||
</PageHeader>
|
||||
<div style={{ display: active ? '' : 'none' }}>
|
||||
<TabContainer defaultValue="api-config">
|
||||
<TabList>
|
||||
<TabButton value="api-config">
|
||||
{t('pages.twitch-settings.api-configuration')}
|
||||
</TabButton>
|
||||
<TabButton value="eventsub">
|
||||
{t('pages.twitch-settings.eventsub')}
|
||||
</TabButton>
|
||||
<TabButton value="chat-settings">
|
||||
{t('pages.twitch-settings.chat-settings')}
|
||||
</TabButton>
|
||||
</TabList>
|
||||
<TabContent value="api-config">
|
||||
<TwitchAPISettings />
|
||||
</TabContent>
|
||||
<TabContent value="eventsub">
|
||||
<TwitchEventSubSettings />
|
||||
</TabContent>
|
||||
<TabContent value="chat-settings">
|
||||
<TwitchChatSettings />
|
||||
</TabContent>
|
||||
</TabContainer>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
import { useState } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useModule, useStatus } from '~/lib/react';
|
||||
import { useAppDispatch } from '~/store';
|
||||
import apiReducer, { modules } from '~/store/api/reducer';
|
||||
import { checkTwitchKeys } from '~/lib/twitch';
|
||||
import BrowserLink from '../../components/BrowserLink';
|
||||
import DefinitionTable from '../../components/DefinitionTable';
|
||||
import RevealLink from '../../components/utils/RevealLink';
|
||||
import SaveButton from '../../components/forms/SaveButton';
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Field,
|
||||
InputBox,
|
||||
Label,
|
||||
PasswordInputBox,
|
||||
SectionHeader,
|
||||
styled,
|
||||
TextBlock,
|
||||
} from '../../theme';
|
||||
import AlertContent from '../../components/AlertContent';
|
||||
import { Alert } from '../../theme/alert';
|
||||
|
||||
const StepList = styled('ul', {
|
||||
lineHeight: '1.5',
|
||||
listStyleType: 'none',
|
||||
listStylePosition: 'outside',
|
||||
});
|
||||
const Step = styled('li', {
|
||||
marginBottom: '0.5rem',
|
||||
paddingLeft: '1rem',
|
||||
'&::marker': {
|
||||
color: '$teal11',
|
||||
content: '▧',
|
||||
display: 'inline-block',
|
||||
marginLeft: '-0.5rem',
|
||||
},
|
||||
});
|
||||
|
||||
type TestResult = { open: boolean; error?: Error };
|
||||
|
||||
export default function TwitchAPISettings() {
|
||||
const { t } = useTranslation();
|
||||
const [httpConfig] = useModule(modules.httpConfig);
|
||||
const [twitchConfig, setTwitchConfig, loadStatus] = useModule(
|
||||
modules.twitchConfig,
|
||||
);
|
||||
const status = useStatus(loadStatus.save);
|
||||
const dispatch = useAppDispatch();
|
||||
const [revealClientSecret, setRevealClientSecret] = useState(false);
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [testResult, setTestResult] = useState<TestResult>({
|
||||
open: false,
|
||||
});
|
||||
|
||||
const checkCredentials = async () => {
|
||||
setTesting(true);
|
||||
if (twitchConfig) {
|
||||
try {
|
||||
await checkTwitchKeys(
|
||||
twitchConfig.api_client_id,
|
||||
twitchConfig.api_client_secret,
|
||||
);
|
||||
setTestResult({ open: true });
|
||||
} catch (e: unknown) {
|
||||
setTestResult({ open: true, error: e as Error });
|
||||
}
|
||||
}
|
||||
setTesting(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(ev) => {
|
||||
void dispatch(setTwitchConfig(twitchConfig));
|
||||
ev.preventDefault();
|
||||
}}
|
||||
>
|
||||
<SectionHeader spacing={'none'}>
|
||||
{t('pages.twitch-settings.api-subheader')}
|
||||
</SectionHeader>
|
||||
<TextBlock>{t('pages.twitch-settings.apiguide-1')}</TextBlock>
|
||||
<StepList>
|
||||
<Step>
|
||||
<Trans i18nKey="pages.twitch-settings.apiguide-2">
|
||||
{' '}
|
||||
<BrowserLink href="https://dev.twitch.tv/console/apps/create">
|
||||
https://dev.twitch.tv/console/apps/create
|
||||
</BrowserLink>
|
||||
</Trans>
|
||||
</Step>
|
||||
<Step>
|
||||
{t('pages.twitch-settings.apiguide-3')}
|
||||
|
||||
<DefinitionTable
|
||||
entries={{
|
||||
[t('pages.twitch-settings.app-oauth-redirect-url')]: `http://${
|
||||
httpConfig?.bind.indexOf(':') > 0
|
||||
? httpConfig.bind
|
||||
: `localhost${httpConfig?.bind ?? ':4337'}`
|
||||
}/twitch/callback`,
|
||||
[t('pages.twitch-settings.app-category')]: 'Broadcasting Suite',
|
||||
}}
|
||||
/>
|
||||
</Step>
|
||||
<Step>
|
||||
<Trans i18nKey="pages.twitch-settings.apiguide-4">
|
||||
{'str1 '}
|
||||
<b>str2</b>
|
||||
</Trans>
|
||||
</Step>
|
||||
</StepList>
|
||||
<Field size="fullWidth" css={{ marginTop: '2rem' }}>
|
||||
<Label htmlFor="clientid">
|
||||
{t('pages.twitch-settings.app-client-id')}
|
||||
</Label>
|
||||
<InputBox
|
||||
type="text"
|
||||
id="clientid"
|
||||
placeholder={t('pages.twitch-settings.app-client-id')}
|
||||
required={true}
|
||||
value={twitchConfig?.api_client_id ?? ''}
|
||||
onChange={(ev) =>
|
||||
dispatch(
|
||||
apiReducer.actions.twitchConfigChanged({
|
||||
...twitchConfig,
|
||||
api_client_id: ev.target.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field size="fullWidth">
|
||||
<Label htmlFor="clientsecret">
|
||||
{t('pages.twitch-settings.app-client-secret')}
|
||||
<RevealLink
|
||||
value={revealClientSecret}
|
||||
setter={setRevealClientSecret}
|
||||
/>
|
||||
</Label>
|
||||
<PasswordInputBox
|
||||
reveal={revealClientSecret}
|
||||
id="clientsecret"
|
||||
placeholder={t('pages.twitch-settings.app-client-secret')}
|
||||
required={true}
|
||||
value={twitchConfig?.api_client_secret ?? ''}
|
||||
onChange={(ev) =>
|
||||
dispatch(
|
||||
apiReducer.actions.twitchConfigChanged({
|
||||
...twitchConfig,
|
||||
api_client_secret: ev.target.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<ButtonGroup>
|
||||
<SaveButton status={status} />
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void checkCredentials();
|
||||
}}
|
||||
disabled={testing}
|
||||
>
|
||||
{t('pages.twitch-settings.test-button')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Alert
|
||||
defaultOpen={false}
|
||||
open={testResult.open}
|
||||
onOpenChange={(val: boolean) => {
|
||||
setTestResult({ ...testResult, open: val });
|
||||
}}
|
||||
>
|
||||
<AlertContent
|
||||
variation={testResult.error ? 'danger' : 'default'}
|
||||
description={
|
||||
testResult.error
|
||||
? t('pages.twitch-settings.test-failed', {
|
||||
error: testResult.error.message,
|
||||
})
|
||||
: t('pages.twitch-settings.test-succeeded')
|
||||
}
|
||||
actionText={t('form-actions.ok')}
|
||||
onAction={() => {
|
||||
setTestResult({ ...testResult, open: false });
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
</form>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { useModule, useStatus } from '~/lib/react';
|
||||
import { useAppDispatch } from '~/store';
|
||||
import apiReducer, { modules } from '~/store/api/reducer';
|
||||
import SaveButton from '../../components/forms/SaveButton';
|
||||
import { Field, InputBox, Label, SectionHeader } from '../../theme';
|
||||
|
||||
export default function TwitchChatSettings() {
|
||||
const [chatConfig, setChatConfig, loadStatus] = useModule(
|
||||
modules.twitchChatConfig,
|
||||
);
|
||||
const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig);
|
||||
const status = useStatus(loadStatus.save);
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const disabled = status?.type === 'pending';
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(ev) => {
|
||||
void dispatch(setTwitchConfig(twitchConfig));
|
||||
void dispatch(setChatConfig(chatConfig));
|
||||
ev.preventDefault();
|
||||
}}
|
||||
>
|
||||
<SectionHeader>
|
||||
{t('pages.twitch-settings.bot-chat-header')}
|
||||
</SectionHeader>
|
||||
<Field size="fullWidth">
|
||||
<Label htmlFor="bot-chat-history">
|
||||
{t('pages.twitch-settings.bot-chat-cooldown-tip')}
|
||||
</Label>
|
||||
<InputBox
|
||||
type="number"
|
||||
id="bot-chat-history"
|
||||
required={true}
|
||||
disabled={disabled}
|
||||
defaultValue={
|
||||
chatConfig ? chatConfig.command_cooldown ?? 2 : undefined
|
||||
}
|
||||
onChange={(ev) =>
|
||||
dispatch(
|
||||
apiReducer.actions.twitchChatConfigChanged({
|
||||
...chatConfig,
|
||||
command_cooldown: parseInt(ev.target.value, 10),
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<SaveButton status={status} />
|
||||
</form>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import { ExternalLinkIcon } from '@radix-ui/react-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import eventsubTests from '~/data/eventsub-tests';
|
||||
import { useAppSelector } from '~/store';
|
||||
import { startAuthFlow } from '~/lib/twitch';
|
||||
import { Button, ButtonGroup, SectionHeader, TextBlock } from '../../theme';
|
||||
import TwitchUserBlock from '../../components/TwitchUserBlock';
|
||||
|
||||
export default function TwitchEventSubSettings() {
|
||||
const { t } = useTranslation();
|
||||
const kv = useAppSelector((state) => state.api.client);
|
||||
|
||||
const sendFakeEvent = async (event: keyof typeof eventsubTests) => {
|
||||
const data = eventsubTests[event];
|
||||
await kv.putJSON(`twitch/ev/eventsub-event/${event}`, {
|
||||
...data,
|
||||
subscription: {
|
||||
...data.subscription,
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
date: new Date().toISOString(),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextBlock>{t('pages.twitch-settings.events.auth-message')}</TextBlock>
|
||||
<Button
|
||||
variation="primary"
|
||||
onClick={() => {
|
||||
void startAuthFlow('stream');
|
||||
}}
|
||||
>
|
||||
<ExternalLinkIcon /> {t('pages.twitch-settings.events.auth-button')}
|
||||
</Button>
|
||||
<SectionHeader>
|
||||
{t('pages.twitch-settings.events.current-status')}
|
||||
</SectionHeader>
|
||||
<TwitchUserBlock authKey={'twitch/auth-keys'} />
|
||||
<SectionHeader>
|
||||
{t('pages.twitch-settings.events.sim-events')}
|
||||
</SectionHeader>
|
||||
<ButtonGroup>
|
||||
{Object.keys(eventsubTests).map((ev: keyof typeof eventsubTests) => (
|
||||
<Button
|
||||
key={ev}
|
||||
onClick={() => {
|
||||
void sendFakeEvent(ev);
|
||||
}}
|
||||
>
|
||||
{t(`pages.twitch-settings.events.sim.${ev}`, { defaultValue: ev })}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
</>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue