import { useEffect, useMemo, useState } from 'react'; import { computePendingCrossTeamReplies } from '@renderer/utils/crossTeamPendingReplies'; import { ChevronRight } from 'lucide-react'; import { ActiveTasksBlock } from '../activity/ActiveTasksBlock'; import { PendingRepliesBlock } from '../activity/PendingRepliesBlock'; import type { InboxMessage, ResolvedTeamMember, TeamTaskWithKanban } from '@shared/types'; interface StatusBlockProps { members: ResolvedTeamMember[]; tasks: TeamTaskWithKanban[]; messages: InboxMessage[]; pendingRepliesByMember: Record; /** Where the Messages panel is rendered — 'sidebar' hides "In progress" (already visible in MemberList). */ position?: 'sidebar' | 'inline'; /** Overlay keeps the toggle hovering over the previous section, flow keeps it in normal layout. */ layout?: 'overlay' | 'flow'; onMemberClick?: (member: ResolvedTeamMember) => void; onTaskClick?: (task: TeamTaskWithKanban) => void; } /** * Self-contained status section that owns its own 1-second timer for * cross-team pending reply TTL tracking. Isolates the timer-driven * re-renders from the rest of MessagesPanel / ActivityTimeline so that * text selection in messages is not disrupted. */ export const StatusBlock = ({ members, tasks, messages, pendingRepliesByMember, position, layout = 'overlay', onMemberClick, onTaskClick, }: StatusBlockProps): React.JSX.Element | null => { const [collapsed, setCollapsed] = useState(false); const [nowMs, setNowMs] = useState(() => Date.now()); const pendingCrossTeamReplies = useMemo( () => computePendingCrossTeamReplies(messages, nowMs), [messages, nowMs] ); const hasPendingReplies = useMemo(() => { const hasMemberPendingReplies = Object.keys(pendingRepliesByMember).some((name) => members.some((m) => m.name === name) ); return hasMemberPendingReplies || pendingCrossTeamReplies.length > 0; }, [members, pendingRepliesByMember, pendingCrossTeamReplies.length]); const hasActiveTasks = useMemo(() => { const tMap = new Map(tasks.map((t) => [t.id, t])); return members.some((m) => { if (!m.currentTaskId) return false; const task = tMap.get(m.currentTaskId); if (task && (task.reviewState === 'approved' || task.status === 'completed')) return false; return true; }); }, [members, tasks]); /** Whether the Status block has any visible items. */ const hasItems = useMemo(() => { if (hasPendingReplies) return true; return hasActiveTasks; }, [hasActiveTasks, hasPendingReplies]); // Only run the 1-second timer when the block actually has content to show. useEffect(() => { if (!hasItems) return; const id = window.setInterval(() => setNowMs(Date.now()), 1000); return () => window.clearInterval(id); }, [hasItems]); if (!hasItems) return null; const toggleButton = ( ); const flowInlineToggle = layout === 'flow' && !collapsed ? toggleButton : null; return ( <> {layout === 'overlay' ? (
{toggleButton}
) : collapsed ? (
{toggleButton}
) : null} {!collapsed && (
{hasPendingReplies ? ( ) : null}
)} ); };