/** * Channels Page * Manage messaging channel connections with configuration UI */ import { useState, useEffect, useCallback } from 'react'; import { RefreshCw, Trash2, AlertCircle } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { useChannelsStore } from '@/stores/channels'; import { useGatewayStore } from '@/stores/gateway'; import { LoadingSpinner } from '@/components/common/LoadingSpinner'; import { hostApiFetch } from '@/lib/host-api'; import { subscribeHostEvent } from '@/lib/host-events'; import { ChannelConfigModal } from '@/components/channels/ChannelConfigModal'; import { cn } from '@/lib/utils'; import { CHANNEL_ICONS, CHANNEL_NAMES, CHANNEL_META, getPrimaryChannels, type ChannelType, type Channel, } from '@/types/channel'; import { useTranslation } from 'react-i18next'; import telegramIcon from '@/assets/channels/telegram.svg'; import discordIcon from '@/assets/channels/discord.svg'; import whatsappIcon from '@/assets/channels/whatsapp.svg'; import dingtalkIcon from '@/assets/channels/dingtalk.svg'; import feishuIcon from '@/assets/channels/feishu.svg'; import wecomIcon from '@/assets/channels/wecom.svg'; import qqIcon from '@/assets/channels/qq.svg'; export function Channels() { const { t } = useTranslation('channels'); const { channels, loading, error, fetchChannels, deleteChannel } = useChannelsStore(); const gatewayStatus = useGatewayStore((state) => state.status); const [showAddDialog, setShowAddDialog] = useState(false); const [selectedChannelType, setSelectedChannelType] = useState(null); const [configuredTypes, setConfiguredTypes] = useState([]); const [channelToDelete, setChannelToDelete] = useState<{ id: string } | null>(null); useEffect(() => { void fetchChannels(); }, [fetchChannels]); const fetchConfiguredTypes = useCallback(async () => { try { const result = await hostApiFetch<{ success: boolean; channels?: string[]; }>('/api/channels/configured'); if (result.success && result.channels) { setConfiguredTypes(result.channels); } } catch { // Ignore refresh errors here and keep the last known state. } }, []); useEffect(() => { const timer = window.setTimeout(() => { void fetchConfiguredTypes(); }, 0); return () => window.clearTimeout(timer); }, [fetchConfiguredTypes]); useEffect(() => { const unsubscribe = subscribeHostEvent('gateway:channel-status', () => { void fetchChannels(); void fetchConfiguredTypes(); }); return () => { if (typeof unsubscribe === 'function') { unsubscribe(); } }; }, [fetchChannels, fetchConfiguredTypes]); const displayedChannelTypes = getPrimaryChannels(); const handleRefresh = () => { void Promise.all([fetchChannels(), fetchConfiguredTypes()]); }; if (loading) { return (
); } const safeChannels = Array.isArray(channels) ? channels : []; return (

{t('title')}

{t('subtitle')}

{gatewayStatus.state !== 'running' && (
{t('gatewayWarning')}
)} {error && (
{error}
)} {safeChannels.length > 0 && (

{t('availableChannels')}

{safeChannels.map((channel) => ( { setSelectedChannelType(channel.type); setShowAddDialog(true); }} onDelete={() => setChannelToDelete({ id: channel.id })} /> ))}
)}

{t('supportedChannels')}

{displayedChannelTypes.map((type) => { const meta = CHANNEL_META[type]; const isConfigured = safeChannels.some((channel) => channel.type === type) || configuredTypes.includes(type); if (isConfigured) return null; return ( ); })}
{showAddDialog && ( { setShowAddDialog(false); setSelectedChannelType(null); }} onChannelSaved={async () => { await Promise.all([fetchChannels(), fetchConfiguredTypes()]); setShowAddDialog(false); setSelectedChannelType(null); }} /> )} { if (channelToDelete) { await deleteChannel(channelToDelete.id); const [channelType] = channelToDelete.id.split('-'); setConfiguredTypes((prev) => prev.filter((type) => type !== channelType)); setChannelToDelete(null); } }} onCancel={() => setChannelToDelete(null)} />
); } function ChannelLogo({ type }: { type: ChannelType }) { switch (type) { case 'telegram': return Telegram; case 'discord': return Discord; case 'whatsapp': return WhatsApp; case 'dingtalk': return DingTalk; case 'feishu': return Feishu; case 'wecom': return WeCom; case 'qqbot': return QQ; default: return {CHANNEL_ICONS[type] || '💬'}; } } interface ChannelCardProps { channel: Channel; onClick: () => void; onDelete: () => void; } function ChannelCard({ channel, onClick, onDelete }: ChannelCardProps) { const { t } = useTranslation('channels'); const meta = CHANNEL_META[channel.type]; return (

{channel.name}

{meta?.isPlugin && ( {t('pluginBadge', 'Plugin')} )}
{channel.error ? (

{channel.error}

) : (

{meta ? t(meta.description.replace('channels:', '')) : CHANNEL_NAMES[channel.type]}

)}
); } export default Channels;