Add comprehensive SignalR client implementation with connection management, React hooks, and UI components for real-time notifications and project updates. Changes: - Install @microsoft/signalr package (v9.0.6) - Create SignalR connection manager with auto-reconnect - Implement useNotificationHub hook for notification hub - Implement useProjectHub hook for project hub with room-based subscriptions - Add NotificationPopover UI component with badge and dropdown - Create Badge UI component - Add SignalRProvider for global connection initialization - Update Header to display real-time notifications - Update app layout to include SignalRProvider - Add comprehensive documentation in SIGNALR_INTEGRATION.md Features: - JWT authentication with automatic token management - Auto-reconnect with exponential backoff (0s, 2s, 5s, 10s, 30s) - Connection state management and indicators - Real-time notification push - Project event subscriptions (create, update, delete, status change) - Room-based project subscriptions - Typing indicators support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
137 lines
3.8 KiB
TypeScript
137 lines
3.8 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState, useCallback, useRef } from 'react';
|
|
import { SignalRConnectionManager } from '@/lib/signalr/ConnectionManager';
|
|
import { SIGNALR_CONFIG } from '@/lib/signalr/config';
|
|
import { useAuthStore } from '@/stores/authStore';
|
|
|
|
export function useProjectHub(projectId?: string) {
|
|
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
|
const [connectionState, setConnectionState] = useState<
|
|
'disconnected' | 'connecting' | 'connected' | 'reconnecting'
|
|
>('disconnected');
|
|
const managerRef = useRef<SignalRConnectionManager | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!isAuthenticated) return;
|
|
|
|
const manager = new SignalRConnectionManager(
|
|
SIGNALR_CONFIG.HUB_URLS.PROJECT
|
|
);
|
|
managerRef.current = manager;
|
|
|
|
const unsubscribe = manager.onStateChange(setConnectionState);
|
|
|
|
// 监听项目事件
|
|
manager.on('ProjectUpdated', (data: any) => {
|
|
console.log('[ProjectHub] Project updated:', data);
|
|
// TODO: 触发项目数据重新加载
|
|
});
|
|
|
|
manager.on('IssueCreated', (issue: any) => {
|
|
console.log('[ProjectHub] Issue created:', issue);
|
|
// TODO: 添加到看板
|
|
});
|
|
|
|
manager.on('IssueUpdated', (issue: any) => {
|
|
console.log('[ProjectHub] Issue updated:', issue);
|
|
// TODO: 更新看板
|
|
});
|
|
|
|
manager.on('IssueDeleted', (data: { IssueId: string }) => {
|
|
console.log('[ProjectHub] Issue deleted:', data);
|
|
// TODO: 从看板移除
|
|
});
|
|
|
|
manager.on('IssueStatusChanged', (data: any) => {
|
|
console.log('[ProjectHub] Issue status changed:', data);
|
|
// TODO: 移动看板卡片
|
|
});
|
|
|
|
manager.on('UserJoinedProject', (data: any) => {
|
|
console.log('[ProjectHub] User joined:', data);
|
|
});
|
|
|
|
manager.on('UserLeftProject', (data: any) => {
|
|
console.log('[ProjectHub] User left:', data);
|
|
});
|
|
|
|
manager.on(
|
|
'TypingIndicator',
|
|
(data: { UserId: string; IssueId: string; IsTyping: boolean }) => {
|
|
console.log('[ProjectHub] Typing indicator:', data);
|
|
// TODO: 显示正在输入提示
|
|
}
|
|
);
|
|
|
|
manager.start();
|
|
|
|
return () => {
|
|
unsubscribe();
|
|
manager.stop();
|
|
};
|
|
}, [isAuthenticated]);
|
|
|
|
// 加入项目房间
|
|
const joinProject = useCallback(async (projectId: string) => {
|
|
if (!managerRef.current) return;
|
|
|
|
try {
|
|
await managerRef.current.invoke('JoinProject', projectId);
|
|
console.log(`[ProjectHub] Joined project ${projectId}`);
|
|
} catch (error) {
|
|
console.error('[ProjectHub] Error joining project:', error);
|
|
}
|
|
}, []);
|
|
|
|
// 离开项目房间
|
|
const leaveProject = useCallback(async (projectId: string) => {
|
|
if (!managerRef.current) return;
|
|
|
|
try {
|
|
await managerRef.current.invoke('LeaveProject', projectId);
|
|
console.log(`[ProjectHub] Left project ${projectId}`);
|
|
} catch (error) {
|
|
console.error('[ProjectHub] Error leaving project:', error);
|
|
}
|
|
}, []);
|
|
|
|
// 发送正在输入指示器
|
|
const sendTypingIndicator = useCallback(
|
|
async (projectId: string, issueId: string, isTyping: boolean) => {
|
|
if (!managerRef.current) return;
|
|
|
|
try {
|
|
await managerRef.current.invoke(
|
|
'SendTypingIndicator',
|
|
projectId,
|
|
issueId,
|
|
isTyping
|
|
);
|
|
} catch (error) {
|
|
console.error('[ProjectHub] Error sending typing indicator:', error);
|
|
}
|
|
},
|
|
[]
|
|
);
|
|
|
|
// 当 projectId 变化时自动加入/离开
|
|
useEffect(() => {
|
|
if (connectionState === 'connected' && projectId) {
|
|
joinProject(projectId);
|
|
|
|
return () => {
|
|
leaveProject(projectId);
|
|
};
|
|
}
|
|
}, [connectionState, projectId, joinProject, leaveProject]);
|
|
|
|
return {
|
|
connectionState,
|
|
joinProject,
|
|
leaveProject,
|
|
sendTypingIndicator,
|
|
isConnected: connectionState === 'connected',
|
|
};
|
|
}
|