From 5db84bd89c8808b0895c7206e8a6a58043f9f8dc Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Sat, 07 Mar 2026 18:04:16 +0100
Subject: [PATCH] feat: heartbeat fix, copy messages, copy/share images, hide unknown duration

---
 services/websocket.ts |   32 ++++++++++++++++----------------
 1 files changed, 16 insertions(+), 16 deletions(-)

diff --git a/services/websocket.ts b/services/websocket.ts
index 4c74e56..895d995 100644
--- a/services/websocket.ts
+++ b/services/websocket.ts
@@ -17,7 +17,7 @@
 const MAX_RECONNECT_DELAY = 30000;
 const RECONNECT_MULTIPLIER = 2;
 const LOCAL_TIMEOUT = 2500;
-const HEARTBEAT_INTERVAL = 20000; // 20s ping to detect zombie sockets
+const HEARTBEAT_INTERVAL = 30000; // 30s ping to detect zombie sockets
 
 export class WebSocketClient {
   private ws: WebSocket | null = null;
@@ -27,22 +27,24 @@
   private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
   private localTimer: ReturnType<typeof setTimeout> | null = null;
   private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
-  private pongReceived: boolean = true;
+  private lastMessageAt: number = 0; // timestamp of last received message (any type)
   private shouldReconnect: boolean = false;
   private connected: boolean = false;
   private callbacks: WebSocketClientOptions = {};
 
   constructor() {
-    // When app comes back to foreground, check if socket is still alive
+    // When app comes back to foreground, verify the connection
     AppState.addEventListener("change", (state: AppStateStatus) => {
       if (state === "active" && this.shouldReconnect) {
-        if (!this.connected || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
-          // Socket is dead — force immediate reconnect
+        if (!this.ws || this.ws.readyState === WebSocket.CLOSED || this.ws.readyState === WebSocket.CLOSING) {
+          // Socket is definitively dead — force immediate reconnect
           this.reconnectDelay = INITIAL_RECONNECT_DELAY;
           this.urlIndex = 0;
           this.tryUrl();
-        } else {
-          // Socket looks open but might be zombie — send a ping to verify
+        } else if (this.connected) {
+          // Socket might be alive — send a ping and restart heartbeat
+          // to give it a fresh 2-interval window to prove liveness
+          this.startHeartbeat();
           this.sendPing();
         }
       }
@@ -80,10 +82,11 @@
 
   private startHeartbeat() {
     this.stopHeartbeat();
-    this.pongReceived = true;
+    this.lastMessageAt = Date.now();
     this.heartbeatTimer = setInterval(() => {
-      if (!this.pongReceived) {
-        // No pong since last ping — socket is zombie, force reconnect
+      const silentMs = Date.now() - this.lastMessageAt;
+      if (silentMs > HEARTBEAT_INTERVAL * 2) {
+        // No message (including pong) for 2 full intervals — zombie socket
         this.connected = false;
         this.callbacks.onClose?.();
         this.reconnectDelay = INITIAL_RECONNECT_DELAY;
@@ -91,6 +94,7 @@
         this.tryUrl();
         return;
       }
+      // Send a ping so the server has something to respond to
       this.sendPing();
     }, HEARTBEAT_INTERVAL);
   }
@@ -101,7 +105,6 @@
 
   private sendPing() {
     if (this.ws && this.ws.readyState === WebSocket.OPEN) {
-      this.pongReceived = false;
       try {
         this.ws.send(JSON.stringify({ type: "ping" }));
       } catch {
@@ -147,13 +150,10 @@
 
     ws.onmessage = (event) => {
       if (ws !== this.ws) return; // stale
+      this.lastMessageAt = Date.now(); // any message proves the connection is alive
       try {
         const data = JSON.parse(event.data) as WebSocketMessage;
-        // Handle pong responses from heartbeat
-        if (data.type === "pong") {
-          this.pongReceived = true;
-          return;
-        }
+        if (data.type === "pong") return; // heartbeat response, don't forward
         this.callbacks.onMessage?.(data);
       } catch {
         this.callbacks.onMessage?.({ type: "text", content: String(event.data) });

--
Gitblit v1.3.1