diff --git a/web/src/components/ReconnectingBanner.tsx b/web/src/components/ReconnectingBanner.tsx index e7a3ad01..76829771 100644 --- a/web/src/components/ReconnectingBanner.tsx +++ b/web/src/components/ReconnectingBanner.tsx @@ -5,6 +5,9 @@ function getReasonLabel(reason: string, t: (key: string) => string): string { if (reason === 'heartbeat-timeout') { return t('reconnecting.reason.heartbeatTimeout') } + if (reason === 'visibility-recovery') { + return t('reconnecting.reason.visibilityRecovery') + } if (reason === 'closed') { return t('reconnecting.reason.closed') } diff --git a/web/src/hooks/useSSE.ts b/web/src/hooks/useSSE.ts index 66f310d2..aa166419 100644 --- a/web/src/hooks/useSSE.ts +++ b/web/src/hooks/useSSE.ts @@ -596,8 +596,22 @@ export function useSSE(options: { requestReconnect('heartbeat-timeout') }, HEARTBEAT_WATCHDOG_INTERVAL_MS) + // When the tab becomes visible again, check immediately whether the + // SSE connection went stale while hidden (the watchdog skips checks + // for hidden tabs). This avoids the user having to wait up to + // HEARTBEAT_WATCHDOG_INTERVAL_MS after switching back. + const onVisibilityChange = () => { + if (getVisibilityState() !== 'visible') return + if (eventSourceRef.current !== eventSource) return + if (Date.now() - lastActivityAtRef.current >= HEARTBEAT_STALE_MS) { + requestReconnect('visibility-recovery') + } + } + document.addEventListener('visibilitychange', onVisibilityChange) + return () => { clearInterval(watchdogTimer) + document.removeEventListener('visibilitychange', onVisibilityChange) if (invalidationTimerRef.current) { clearTimeout(invalidationTimerRef.current) invalidationTimerRef.current = null diff --git a/web/src/lib/locales/en.ts b/web/src/lib/locales/en.ts index f2612610..c8eff291 100644 --- a/web/src/lib/locales/en.ts +++ b/web/src/lib/locales/en.ts @@ -237,6 +237,7 @@ export default { 'reconnecting.reason.error': 'stream error', 'reconnecting.reason.closed': 'stream closed', 'reconnecting.reason.heartbeatTimeout': 'heartbeat timeout', + 'reconnecting.reason.visibilityRecovery': 'resuming after background', // Send blocked 'send.blocked.title': 'Cannot send message', diff --git a/web/src/lib/locales/zh-CN.ts b/web/src/lib/locales/zh-CN.ts index ea220f5a..32eaff2f 100644 --- a/web/src/lib/locales/zh-CN.ts +++ b/web/src/lib/locales/zh-CN.ts @@ -239,6 +239,7 @@ export default { 'reconnecting.reason.error': '流连接错误', 'reconnecting.reason.closed': '流连接已关闭', 'reconnecting.reason.heartbeatTimeout': '心跳超时', + 'reconnecting.reason.visibilityRecovery': '后台恢复中', // Send blocked 'send.blocked.title': '无法发送消息',