import { memo, useEffect, useRef, useState } from 'react'; import { useAppTranslation } from '@features/localization/renderer'; import { SyncedLoader2 } from '@renderer/components/ui/SyncedLoader2'; import { formatMemberActivityElapsed, readMemberActivityTimerElapsed, syncMemberActivityTimer, } from '@renderer/utils/memberActivityTimer'; import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; import type { MemberActivityTimerAnchor } from '@renderer/utils/memberActivityTimer'; import type { TeamTaskWithKanban } from '@shared/types'; interface CurrentTaskIndicatorProps { task: TeamTaskWithKanban; borderColor: string; maxSubjectLength?: number; activityLabel?: string; activityTimer?: MemberActivityTimerAnchor | null; isTimerRunning?: boolean; onOpenTask?: () => void; } const ACTIVITY_TIMER_STORAGE_SYNC_INTERVAL_MS = 5_000; function useActivityTimerLabel( activityTimer: MemberActivityTimerAnchor | null | undefined, isTimerRunning: boolean ): string | null { const [nowMs, setNowMs] = useState(() => Date.now()); const lastStorageSyncAtRef = useRef(0); useEffect(() => { if (!activityTimer) return; const now = Date.now(); lastStorageSyncAtRef.current = now; syncMemberActivityTimer({ timerId: activityTimer.timerId, startedAtMs: activityTimer.startedAtMs, baseElapsedMs: activityTimer.baseElapsedMs, running: isTimerRunning, runId: activityTimer.runId, nowMs: now, }); return () => { syncMemberActivityTimer({ timerId: activityTimer.timerId, startedAtMs: activityTimer.startedAtMs, baseElapsedMs: activityTimer.baseElapsedMs, running: isTimerRunning, runId: activityTimer.runId, nowMs: Date.now(), }); }; }, [activityTimer, isTimerRunning]); useEffect(() => { if (!activityTimer || !isTimerRunning) return; const handle = window.setInterval(() => { const now = Date.now(); if (now - lastStorageSyncAtRef.current >= ACTIVITY_TIMER_STORAGE_SYNC_INTERVAL_MS) { lastStorageSyncAtRef.current = now; syncMemberActivityTimer({ timerId: activityTimer.timerId, startedAtMs: activityTimer.startedAtMs, baseElapsedMs: activityTimer.baseElapsedMs, running: true, runId: activityTimer.runId, nowMs: now, }); } setNowMs(now); }, 1000); return () => window.clearInterval(handle); }, [activityTimer, isTimerRunning]); if (!activityTimer) return null; return formatMemberActivityElapsed( readMemberActivityTimerElapsed({ timerId: activityTimer.timerId, startedAtMs: activityTimer.startedAtMs, baseElapsedMs: activityTimer.baseElapsedMs, running: isTimerRunning, runId: activityTimer.runId, nowMs, }) ); } /** * Inline indicator showing a spinning loader + "working on" + task label button. * Shared between MemberCard and MemberHoverCard. */ export const CurrentTaskIndicator = memo( ({ task, borderColor, maxSubjectLength, activityLabel = 'working on', activityTimer, isTimerRunning = true, onOpenTask, }: CurrentTaskIndicatorProps): React.JSX.Element => { const { t } = useAppTranslation('team'); const timerLabel = useActivityTimerLabel(activityTimer, isTimerRunning); const subjectText = typeof maxSubjectLength === 'number' && maxSubjectLength > 0 && task.subject.length > maxSubjectLength ? `${task.subject.slice(0, maxSubjectLength)}…` : task.subject; return (
{activityLabel} {timerLabel ? ( {timerLabel} ) : null}
); } ); CurrentTaskIndicator.displayName = 'CurrentTaskIndicator';