diff --git a/frontend/src/lib/react-utils.ts b/frontend/src/lib/react-utils.ts index 029d2f7..0ddc0e7 100644 --- a/frontend/src/lib/react-utils.ts +++ b/frontend/src/lib/react-utils.ts @@ -62,30 +62,7 @@ export function useUserPoints(): LoyaltyStorage { return data; } -export function useInterval( - initialValue: number, -): [ - number, - number, - number, - (newNum: number) => void, - (newMult: number) => void, -] { - const [value, setValue] = useState(initialValue); - - const [numInitialValue, multInitialValue] = getInterval(value); - const [num, setNum] = useState(numInitialValue); - const [mult, setMult] = useState(multInitialValue); - - useEffect(() => { - setValue(num * mult); - }, [num, mult]); - - return [value, num, mult, setNum, setMult]; -} - export default { useModule, useUserPoints, - useInterval, }; diff --git a/frontend/src/locale/en/translation.json b/frontend/src/locale/en/translation.json index 27c0ab8..12b8b51 100644 --- a/frontend/src/locale/en/translation.json +++ b/frontend/src/locale/en/translation.json @@ -6,6 +6,7 @@ "/twitch/settings": "Module configuration", "/twitch/bot/settings": "Bot configuration", "/twitch/bot/commands": "Bot commands", + "/twitch/bot/timers": "Bot timers", "/twitch/bot/modules": "Bot modules", "/loyalty": "Loyalty points", "/loyalty/settings": "Configuration", @@ -204,6 +205,20 @@ "new-command": "New command", "search": "Search by name", "modify-command": "Modify command" + }, + "timers": { + "header": "Bot timers", + "new-timer": "New timer", + "modify-timer": "Modify timer", + "search": "Search by timer name", + "messages": "Messages", + "name-hint": "Timer name", + "name": "Name", + "message-help": "What to write in chat", + "minimum-delay": "Interval", + "minimum-delay-help": "How many time must pass between each repeat.", + "minimum-activity": "Minimum chat activity", + "minimum-activity-post": "messages in the last 5 minutes" } } } diff --git a/frontend/src/store/api/reducer.ts b/frontend/src/store/api/reducer.ts index 40cf4dd..ed6129c 100644 --- a/frontend/src/store/api/reducer.ts +++ b/frontend/src/store/api/reducer.ts @@ -147,6 +147,13 @@ export const modules = { state.moduleConfigs.twitchBotConfig = payload; }, ), + twitchBotModulesConfig: makeModule( + 'twitch/bot-modules/config', + (state) => state.twitchBot?.modules, + (state, { payload }) => { + state.twitchBot.modules = payload; + }, + ), twitchBotCommands: makeModule( 'twitch/bot-custom-commands', (state) => state.twitchBot?.commands, @@ -154,6 +161,13 @@ export const modules = { state.twitchBot.commands = payload; }, ), + twitchBotTimers: makeModule( + 'twitch/bot-modules/timers/config', + (state) => state.twitchBot?.timers, + (state, { payload }) => { + state.twitchBot.timers = payload; + }, + ), stulbeConfig: makeModule( 'stulbe/config', (state) => state.moduleConfigs?.stulbeConfig, @@ -229,6 +243,8 @@ const initialState: APIState = { }, twitchBot: { commands: null, + modules: null, + timers: null, }, moduleConfigs: { moduleConfig: null, diff --git a/frontend/src/store/api/types.ts b/frontend/src/store/api/types.ts index 161a724..2e4bc76 100644 --- a/frontend/src/store/api/types.ts +++ b/frontend/src/store/api/types.ts @@ -30,6 +30,10 @@ interface TwitchBotConfig { chat_history: number; } +interface TwitchModulesConfig { + enable_timers: boolean; +} + type AccessLevelType = 'everyone' | 'vip' | 'moderators' | 'streamer'; export interface TwitchBotCustomCommand { @@ -57,6 +61,18 @@ interface LoyaltyConfig { banlist: string[]; } +export interface TwitchBotTimer { + enabled: boolean; + name: string; + minimum_chat_activity: number; + minimum_delay: number; + messages: string[]; +} + +interface TwitchBotTimersConfig { + timers: Record; +} + export interface LoyaltyPointsEntry { points: number; } @@ -105,6 +121,8 @@ export interface APIState { }; twitchBot: { commands: TwitchBotCustomCommands; + modules: TwitchModulesConfig; + timers: TwitchBotTimersConfig; }; moduleConfigs: { moduleConfig: ModuleConfig; diff --git a/frontend/src/ui/App.tsx b/frontend/src/ui/App.tsx index 86b6940..678da15 100644 --- a/frontend/src/ui/App.tsx +++ b/frontend/src/ui/App.tsx @@ -22,6 +22,7 @@ import TwitchBotCommandsPage from './pages/twitch/Commands'; import TwitchBotModulesPage from './pages/twitch/Modules'; import StulbeConfigPage from './pages/stulbe/Config'; import StulbeWebhooksPage from './pages/stulbe/Webhook'; +import TwitchBotTimersPage from './pages/twitch/Timers'; interface RouteItem { name?: string; @@ -38,6 +39,7 @@ const menu: RouteItem[] = [ { route: '/twitch/settings' }, { route: '/twitch/bot/settings' }, { route: '/twitch/bot/commands' }, + { route: '/twitch/bot/timers' }, { route: '/twitch/bot/modules' }, ], }, @@ -127,6 +129,7 @@ export default function App(): React.ReactElement { + diff --git a/frontend/src/ui/components/Interval.tsx b/frontend/src/ui/components/Interval.tsx index d4bc92d..6f034a9 100644 --- a/frontend/src/ui/components/Interval.tsx +++ b/frontend/src/ui/components/Interval.tsx @@ -1,19 +1,29 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useInterval } from '../../lib/react-utils'; +import { getInterval } from '../../lib/time-utils'; export interface IntervalProps { active: boolean; value: number; + min?: number; onChange?: (value: number) => void; } -function Interval({ active, value, onChange }: IntervalProps) { +function Interval({ active, value, min, onChange }: IntervalProps) { const { t } = useTranslation(); - const [valueNum, num, mult, setNum, setMult] = useInterval(value); + + const [numInitialValue, multInitialValue] = getInterval(value); + const [num, setNum] = useState(numInitialValue); + const [mult, setMult] = useState(multInitialValue); + useEffect(() => { - onChange(valueNum); - }, [valueNum]); + const seconds = num * mult; + if (min && seconds < min) { + setNum(5); + setMult(1); + } + onChange(Math.max(min ?? 0, seconds)); + }, [num, mult]); return ( <> @@ -24,6 +34,7 @@ function Interval({ active, value, onChange }: IntervalProps) { type="number" placeholder="#" value={num ?? ''} + style={{ width: '6em' }} onChange={(ev) => { const intNum = parseInt(ev.target.value, 10); if (Number.isNaN(intNum)) { diff --git a/frontend/src/ui/pages/loyalty/Rewards.tsx b/frontend/src/ui/pages/loyalty/Rewards.tsx index 2469066..ad12ee2 100644 --- a/frontend/src/ui/pages/loyalty/Rewards.tsx +++ b/frontend/src/ui/pages/loyalty/Rewards.tsx @@ -3,7 +3,7 @@ import React, { useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import prettyTime from 'pretty-ms'; import { useTranslation } from 'react-i18next'; -import { useInterval, useModule } from '../../../lib/react-utils'; +import { useModule } from '../../../lib/react-utils'; import { RootState } from '../../../store'; import { createRedeem, modules } from '../../../store/api/reducer'; import Modal from '../../components/Modal'; diff --git a/frontend/src/ui/pages/loyalty/Settings.tsx b/frontend/src/ui/pages/loyalty/Settings.tsx index f55fa36..cb2ac57 100644 --- a/frontend/src/ui/pages/loyalty/Settings.tsx +++ b/frontend/src/ui/pages/loyalty/Settings.tsx @@ -122,7 +122,12 @@ export default function LoyaltySettingPage( {t('loyalty.config.points-every')}

- + diff --git a/frontend/src/ui/pages/twitch/Timers.tsx b/frontend/src/ui/pages/twitch/Timers.tsx new file mode 100644 index 0000000..68065f3 --- /dev/null +++ b/frontend/src/ui/pages/twitch/Timers.tsx @@ -0,0 +1,319 @@ +import { RouteComponentProps } from '@reach/router'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; +import { useModule } from '../../../lib/react-utils'; +import { modules } from '../../../store/api/reducer'; +import Modal from '../../components/Modal'; +import { TwitchBotTimer } from '../../../store/api/types'; +import Field from '../../components/Field'; +import Interval from '../../components/Interval'; + +interface TimerItemProps { + item: TwitchBotTimer; + onToggleState: () => void; + onEdit: () => void; + onDelete: () => void; +} +function TimerItem({ item, onToggleState, onEdit, onDelete }: TimerItemProps) { + const { t } = useTranslation(); + const [expanded, setExpanded] = useState(false); + + return ( +
+
+
+ {item.enabled ? ( + {item.name} + ) : ( + + {item.name} + + )} +
+ setExpanded(!expanded)} + > + + ❯ + + +
+ {expanded ? ( +
+ {t('twitch.timers.messages')}:{' '} + {item.messages.map((message) => ( +
{message}
+ ))} + +
+ ) : null} +
+ ); +} + +interface TimerModalProps { + active: boolean; + onConfirm: (newName: string, r: TwitchBotTimer) => void; + onClose: () => void; + initialData?: TwitchBotTimer; + initialName?: string; + title: string; + confirmText: string; +} + +function TimerModal({ + active, + onConfirm, + onClose, + initialName, + initialData, + title, + confirmText, +}: TimerModalProps) { + const [name, setName] = useState(initialName ?? ''); + const [messages, setMessages] = useState(initialData?.messages ?? ['']); + const [minDelay, setMinDelay] = useState(initialData?.minimum_delay ?? 300); + const [minActivity, setMinActivity] = useState( + initialData?.minimum_chat_activity ?? 5, + ); + + const { t } = useTranslation(); + const validForm = name !== '' && messages.length > 0 && messages[0] !== ''; + + const confirm = () => { + if (onConfirm) { + onConfirm(name, { + name, + messages, + minimum_chat_activity: 0, + minimum_delay: 0, + enabled: initialData?.enabled ?? false, + }); + } + }; + + const setMessageIndex = (value: string, index: number) => { + const newMessages = [...messages]; + newMessages[index] = value; + setMessages(newMessages); + }; + + return ( + confirm()} + onClose={() => onClose()} + > + +
+
+

+ setName(ev.target.value)} + /> +

+
+
+
+ +
+
+ +
+
+
+ +
+
+

+ { + const amount = parseInt(ev.target.value, 10); + if (Number.isNaN(amount)) { + return; + } + setMinActivity(amount); + }} + /> +

+

+ + {t('twitch.timers.minimum-activity-post')} + +

+
+
+
+ +
+ {messages.map((message, index) => ( +
+

+