refactor: move logic around!

This commit is contained in:
Ash Keel 2024-04-02 23:15:57 +02:00
parent 07e3a00990
commit 3d0e824b4b
No known key found for this signature in database
GPG Key ID: 53A9E9A6035DD109
9 changed files with 494 additions and 533 deletions

View File

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

View File

@ -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';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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