From f744a3e34c1016c0095299797f12d659a231e1a4 Mon Sep 17 00:00:00 2001
From: Kacy Fortner
Date: Sun, 5 Apr 2026 13:09:33 -0400
Subject: [PATCH 1/3] fix: tighten web chat status and shell updates
---
web/public/sw.js | 59 +++++++++++++++++--------
web/src/App.tsx | 111 ++++++++++++++++++++++++++++++++++++-----------
2 files changed, 127 insertions(+), 43 deletions(-)
diff --git a/web/public/sw.js b/web/public/sw.js
index 3bebb08..a313fe1 100644
--- a/web/public/sw.js
+++ b/web/public/sw.js
@@ -1,4 +1,4 @@
-const cacheName = 'imsg-bridge-web-shell-v1';
+const cacheName = 'imsg-bridge-web-shell-v2';
const shellAssets = ['/', '/manifest.webmanifest', '/icon.svg'];
self.addEventListener('install', (event) => {
@@ -33,21 +33,44 @@ self.addEventListener('fetch', (event) => {
return;
}
- event.respondWith(
- caches.match(event.request).then((cached) => {
- if (cached) {
- return cached;
- }
-
- return fetch(event.request).then((response) => {
- if (!response.ok || response.type === 'opaque') {
- return response;
- }
-
- const clone = response.clone();
- caches.open(cacheName).then((cache) => cache.put(event.request, clone));
- return response;
- });
- }),
- );
+ const isShellRequest =
+ event.request.mode === 'navigate' || shellAssets.includes(requestUrl.pathname);
+
+ if (isShellRequest) {
+ event.respondWith(networkFirst(event.request));
+ return;
+ }
+
+ event.respondWith(cacheFirst(event.request));
});
+
+async function networkFirst(request) {
+ try {
+ const response = await fetch(request);
+ if (response.ok && response.type !== 'opaque') {
+ const cache = await caches.open(cacheName);
+ await cache.put(request, response.clone());
+ }
+ return response;
+ } catch (error) {
+ const cached = await caches.match(request);
+ if (cached) {
+ return cached;
+ }
+ throw error;
+ }
+}
+
+async function cacheFirst(request) {
+ const cached = await caches.match(request);
+ if (cached) {
+ return cached;
+ }
+
+ const response = await fetch(request);
+ if (response.ok && response.type !== 'opaque') {
+ const cache = await caches.open(cacheName);
+ await cache.put(request, response.clone());
+ }
+ return response;
+}
diff --git a/web/src/App.tsx b/web/src/App.tsx
index fcbd961..cf79464 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -670,6 +670,7 @@ function AppShell({ profile }: { profile: StoredServerProfile }) {
() => chats.find((chat) => chat.id === activeChatId) ?? null,
[activeChatId, chats],
);
+ const canConnectEvents = Boolean(token) && (status === 'ready' || status === 'refreshing');
useEffect(() => {
chatsRef.current = chats;
@@ -685,7 +686,7 @@ function AppShell({ profile }: { profile: StoredServerProfile }) {
// websocket
useEffect(() => {
- if (!token || (status !== 'ready' && status !== 'refreshing')) {
+ if (!token || !canConnectEvents) {
setEventsStatus('idle');
setEventsError(null);
return;
@@ -695,6 +696,7 @@ function AppShell({ profile }: { profile: StoredServerProfile }) {
let socket: WebSocket | null = null;
let reconnectTimer = 0;
let reconnectDelay = 1000;
+ let lastCloseMessage: string | null = null;
const connect = () => {
if (cancelled) {
@@ -714,6 +716,7 @@ function AppShell({ profile }: { profile: StoredServerProfile }) {
return;
}
reconnectDelay = 1000;
+ lastCloseMessage = null;
setEventsStatus('live');
setEventsError(null);
};
@@ -742,20 +745,31 @@ function AppShell({ profile }: { profile: StoredServerProfile }) {
if (cancelled) {
return;
}
+ lastCloseMessage = 'live updates could not connect. retrying…';
setEventsStatus('error');
- setEventsError('live event stream dropped. retrying…');
+ setEventsError(lastCloseMessage);
};
- socket.onclose = () => {
+ socket.onclose = (event) => {
if (cancelled) {
return;
}
- setEventsStatus('connecting');
+ const closeMessage = describeEventStreamClose(event, lastCloseMessage);
+ const nextDelay = reconnectDelay;
+
+ setEventsStatus('error');
+ setEventsError(closeMessage);
+
reconnectTimer = window.setTimeout(() => {
+ if (cancelled) {
+ return;
+ }
reconnectDelay = Math.min(reconnectDelay * 2, 15000);
+ setEventsStatus('connecting');
+ setEventsError(null);
connect();
- }, reconnectDelay);
+ }, nextDelay);
};
};
@@ -766,7 +780,7 @@ function AppShell({ profile }: { profile: StoredServerProfile }) {
window.clearTimeout(reconnectTimer);
socket?.close();
};
- }, [profile.wsBaseUrl, status, token]);
+ }, [canConnectEvents, profile.wsBaseUrl, token]);
// initial load
useEffect(() => {
@@ -1020,7 +1034,7 @@ function AppShell({ profile }: { profile: StoredServerProfile }) {
messages
-
+
{/* thread — hidden on mobile when chat list is shown */}
@@ -1061,6 +1080,7 @@ function AppShell({ profile }: { profile: StoredServerProfile }) {
onBack={() => setMobileShowThread(false)}
status={threadStatus}
threadError={threadError}
+ eventsError={eventsError}
eventsStatus={eventsStatus}
/>
@@ -1143,6 +1163,7 @@ function ThreadView(props: {
threadError: string | null;
onReload: () => void;
onBack: () => void;
+ eventsError: string | null;
eventsStatus: 'idle' | 'connecting' | 'live' | 'error';
}) {
const scrollRef = useRef(null);
@@ -1172,7 +1193,7 @@ function ThreadView(props: {
{props.activeChat ? displayChatName(props.activeChat) : 'select a chat'}
-
+