feat(desktop): add global channels store with zustand
- Create ChannelsStore for global channel state management - Refactor useChannels hook to use the store - Initialize channels store at app startup - Share state between onboarding and channels page Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
07b8a014aa
commit
5ae86feb2b
3 changed files with 175 additions and 97 deletions
|
|
@ -10,6 +10,8 @@ import ChannelsPage from './pages/channels'
|
|||
import CronsPage from './pages/crons'
|
||||
import OnboardingPage from './pages/onboarding'
|
||||
import { useOnboardingStore } from './stores/onboarding'
|
||||
import { useProviderStore } from './stores/provider'
|
||||
import { useChannelsStore } from './stores/channels'
|
||||
|
||||
function OnboardingGuard({ children }: { children: React.ReactNode }) {
|
||||
const completed = useOnboardingStore((s) => s.completed)
|
||||
|
|
@ -47,6 +49,9 @@ const router = createHashRouter([
|
|||
export default function App() {
|
||||
useEffect(() => {
|
||||
useOnboardingStore.getState().initForceFlag()
|
||||
// Prefetch global data at app startup
|
||||
useProviderStore.getState().fetch()
|
||||
useChannelsStore.getState().fetch()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
/**
|
||||
* Hook for managing channel accounts (Telegram, Discord, etc.) in the Desktop App.
|
||||
*
|
||||
* Provides state and actions for the Channels settings page:
|
||||
* - List channel account states (running / stopped / error)
|
||||
* - Read channel config (tokens)
|
||||
* - Save / remove tokens with immediate start/stop
|
||||
* Uses the global ChannelsStore for state management.
|
||||
* Data is fetched once at app startup and shared across all components.
|
||||
*/
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useChannelsStore } from '../stores/channels'
|
||||
|
||||
export interface UseChannelsReturn {
|
||||
/** Runtime states of all channel accounts */
|
||||
|
|
@ -24,102 +22,23 @@ export interface UseChannelsReturn {
|
|||
/** Remove a bot token — stops the bot and removes from file */
|
||||
removeToken: (channelId: string, accountId: string) => Promise<{ ok: boolean; error?: string }>
|
||||
/** Stop a channel account without removing config */
|
||||
stopChannel: (channelId: string, accountId: string) => Promise<void>
|
||||
stopChannel: (channelId: string, accountId: string) => Promise<{ ok: boolean; error?: string }>
|
||||
/** Start a channel account from saved config */
|
||||
startChannel: (channelId: string, accountId: string) => Promise<void>
|
||||
startChannel: (channelId: string, accountId: string) => Promise<{ ok: boolean; error?: string }>
|
||||
}
|
||||
|
||||
export function useChannels(): UseChannelsReturn {
|
||||
const [states, setStates] = useState<ChannelAccountStateInfo[]>([])
|
||||
const [config, setConfig] = useState<Record<string, Record<string, Record<string, unknown>> | undefined>>({})
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const [stateList, channelConfig] = await Promise.all([
|
||||
window.electronAPI.channels.listStates(),
|
||||
window.electronAPI.channels.getConfig(),
|
||||
])
|
||||
|
||||
setStates(stateList)
|
||||
setConfig(channelConfig)
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
setError(message)
|
||||
console.error('[useChannels] Failed to load:', message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
refresh()
|
||||
}, [refresh])
|
||||
|
||||
const saveToken = useCallback(async (channelId: string, accountId: string, token: string) => {
|
||||
setError(null)
|
||||
try {
|
||||
const result = await window.electronAPI.channels.saveToken(channelId, accountId, token)
|
||||
if (!result.ok) {
|
||||
setError(result.error ?? 'Failed to save token')
|
||||
}
|
||||
// Refresh to pick up new state
|
||||
await refresh()
|
||||
return result
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
setError(message)
|
||||
return { ok: false, error: message }
|
||||
}
|
||||
}, [refresh])
|
||||
|
||||
const removeToken = useCallback(async (channelId: string, accountId: string) => {
|
||||
setError(null)
|
||||
try {
|
||||
const result = await window.electronAPI.channels.removeToken(channelId, accountId)
|
||||
if (!result.ok) {
|
||||
setError(result.error ?? 'Failed to remove token')
|
||||
}
|
||||
await refresh()
|
||||
return result
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
setError(message)
|
||||
return { ok: false, error: message }
|
||||
}
|
||||
}, [refresh])
|
||||
|
||||
const stopChannel = useCallback(async (channelId: string, accountId: string) => {
|
||||
setError(null)
|
||||
try {
|
||||
const result = await window.electronAPI.channels.stop(channelId, accountId)
|
||||
if (!result.ok) {
|
||||
setError(result.error ?? 'Failed to stop channel')
|
||||
}
|
||||
await refresh()
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
setError(message)
|
||||
}
|
||||
}, [refresh])
|
||||
|
||||
const startChannel = useCallback(async (channelId: string, accountId: string) => {
|
||||
setError(null)
|
||||
try {
|
||||
const result = await window.electronAPI.channels.start(channelId, accountId)
|
||||
if (!result.ok) {
|
||||
setError(result.error ?? 'Failed to start channel')
|
||||
}
|
||||
await refresh()
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
setError(message)
|
||||
}
|
||||
}, [refresh])
|
||||
const {
|
||||
states,
|
||||
config,
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
saveToken,
|
||||
removeToken,
|
||||
stopChannel,
|
||||
startChannel,
|
||||
} = useChannelsStore()
|
||||
|
||||
return {
|
||||
states,
|
||||
|
|
|
|||
154
apps/desktop/src/renderer/src/stores/channels.ts
Normal file
154
apps/desktop/src/renderer/src/stores/channels.ts
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
import { create } from 'zustand'
|
||||
|
||||
interface ChannelsStore {
|
||||
// State
|
||||
states: ChannelAccountStateInfo[]
|
||||
config: Record<string, Record<string, Record<string, unknown>> | undefined>
|
||||
loading: boolean
|
||||
error: string | null
|
||||
initialized: boolean
|
||||
|
||||
// Actions
|
||||
fetch: () => Promise<void>
|
||||
refresh: () => Promise<void>
|
||||
saveToken: (channelId: string, accountId: string, token: string) => Promise<{ ok: boolean; error?: string }>
|
||||
removeToken: (channelId: string, accountId: string) => Promise<{ ok: boolean; error?: string }>
|
||||
stopChannel: (channelId: string, accountId: string) => Promise<{ ok: boolean; error?: string }>
|
||||
startChannel: (channelId: string, accountId: string) => Promise<{ ok: boolean; error?: string }>
|
||||
}
|
||||
|
||||
export const useChannelsStore = create<ChannelsStore>()((set, get) => ({
|
||||
states: [],
|
||||
config: {},
|
||||
loading: false,
|
||||
error: null,
|
||||
initialized: false,
|
||||
|
||||
fetch: async () => {
|
||||
// Skip if already initialized
|
||||
if (get().initialized) return
|
||||
|
||||
set({ loading: true, error: null })
|
||||
|
||||
try {
|
||||
const [stateList, channelConfig] = await Promise.all([
|
||||
window.electronAPI.channels.listStates(),
|
||||
window.electronAPI.channels.getConfig(),
|
||||
])
|
||||
|
||||
set({
|
||||
states: stateList,
|
||||
config: channelConfig,
|
||||
initialized: true,
|
||||
})
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
set({ error: message })
|
||||
console.error('[ChannelsStore] Failed to load:', message)
|
||||
} finally {
|
||||
set({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
refresh: async () => {
|
||||
set({ loading: true, error: null })
|
||||
|
||||
try {
|
||||
const [stateList, channelConfig] = await Promise.all([
|
||||
window.electronAPI.channels.listStates(),
|
||||
window.electronAPI.channels.getConfig(),
|
||||
])
|
||||
|
||||
set({
|
||||
states: stateList,
|
||||
config: channelConfig,
|
||||
})
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
set({ error: message })
|
||||
console.error('[ChannelsStore] Failed to refresh:', message)
|
||||
} finally {
|
||||
set({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
saveToken: async (channelId: string, accountId: string, token: string) => {
|
||||
set({ error: null })
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.channels.saveToken(channelId, accountId, token)
|
||||
|
||||
if (result.ok) {
|
||||
await get().refresh()
|
||||
return { ok: true }
|
||||
} else {
|
||||
set({ error: result.error ?? 'Failed to save token' })
|
||||
return { ok: false, error: result.error }
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
set({ error: message })
|
||||
return { ok: false, error: message }
|
||||
}
|
||||
},
|
||||
|
||||
removeToken: async (channelId: string, accountId: string) => {
|
||||
set({ error: null })
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.channels.removeToken(channelId, accountId)
|
||||
|
||||
if (result.ok) {
|
||||
await get().refresh()
|
||||
return { ok: true }
|
||||
} else {
|
||||
set({ error: result.error ?? 'Failed to remove token' })
|
||||
return { ok: false, error: result.error }
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
set({ error: message })
|
||||
return { ok: false, error: message }
|
||||
}
|
||||
},
|
||||
|
||||
stopChannel: async (channelId: string, accountId: string) => {
|
||||
set({ error: null })
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.channels.stop(channelId, accountId)
|
||||
|
||||
if (result.ok) {
|
||||
await get().refresh()
|
||||
return { ok: true }
|
||||
} else {
|
||||
set({ error: result.error ?? 'Failed to stop channel' })
|
||||
return { ok: false, error: result.error }
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
set({ error: message })
|
||||
return { ok: false, error: message }
|
||||
}
|
||||
},
|
||||
|
||||
startChannel: async (channelId: string, accountId: string) => {
|
||||
set({ error: null })
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.channels.start(channelId, accountId)
|
||||
|
||||
if (result.ok) {
|
||||
await get().refresh()
|
||||
return { ok: true }
|
||||
} else {
|
||||
set({ error: result.error ?? 'Failed to start channel' })
|
||||
return { ok: false, error: result.error }
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
set({ error: message })
|
||||
return { ok: false, error: message }
|
||||
}
|
||||
},
|
||||
}))
|
||||
Loading…
Add table
Add a link
Reference in a new issue