fix: editable decrypt + windows realtime wake (v0.7.1)
Release desktop app / build (, windows-latest) (push) Has been cancelled
Release desktop app / build (--target universal-apple-darwin --bundles app,updater, macos-14) (push) Has been cancelled

Edit message decrypt:
- handleUpdate in useConversationMessages now refetches the canonical row
  via REST after a realtime UPDATE instead of trusting the realtime
  payload's bytea encoding. Same pattern as handleInsert — base64 vs
  `\x…` hex serialisation varies across supabase/postgrest versions and
  was silently producing undecryptable ciphertext for edited messages
  on the receiver side

Windows WebView2 background throttling:
- ConversationsContext, useFriendships and useConversationMessages now
  listen for visibilitychange / focus / online events and trigger both a
  fresh REST refresh and a best-effort channel.subscribe() on wake.
  WebView2 aggressively throttles background WebSockets and was dropping
  realtime events entirely while the window was minimised, so new
  messages and friend acceptances only surfaced after a manual reload

Bump tauri version 0.7.0 -> 0.7.1
This commit is contained in:
2026-04-20 22:15:49 +02:00
parent 37becba7e2
commit a4c9b959a9
4 changed files with 118 additions and 5 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ChatApp",
"version": "0.7.0",
"version": "0.7.1",
"identifier": "com.meinname.chatapp",
"build": {
"beforeDevCommand": "pnpm vite:dev",
@@ -213,7 +213,31 @@ export function ConversationsProvider({ children }: { children: ReactNode }) {
)
.subscribe();
// Windows WebView2 aggressively throttles background WebSockets and
// sometimes drops events entirely while the window is minimised. Force a
// refresh + realtime reconnect on visibility/focus regain so we never
// leave stale conversation lists on a Windows client after the user
// returns to the app.
const onAwake = () => {
if (document.visibilityState !== 'visible') return;
void refresh();
try {
// If the socket got wedged during background throttle, a no-op
// unsubscribe+resubscribe brings it back. `subscribe()` on an already
// joined channel is a no-op so this is safe.
channel.subscribe();
} catch {
/* ignore — already live */
}
};
document.addEventListener('visibilitychange', onAwake);
window.addEventListener('focus', onAwake);
window.addEventListener('online', onAwake);
return () => {
document.removeEventListener('visibilitychange', onAwake);
window.removeEventListener('focus', onAwake);
window.removeEventListener('online', onAwake);
void supabase.removeChannel(channel);
};
}, [myId, refresh, markRead]);
@@ -192,18 +192,71 @@ export function useConversationMessages({ conversationId, userId, deviceId }: Ar
return { ...prev, messages: next };
});
if (partial.editedAt && !partial.deletedAt) {
const [decrypted] = await decryptBatch([partial]);
// Realtime bytea encoding varies (base64 vs hex, even null for
// unchanged columns on some configs). Refetch via REST to get the
// canonical `\x…` hex then decrypt — same pattern as handleInsert.
if (!conversationId || !deviceId) return;
let decrypted: DecryptedMessage | null = null;
for (let attempt = 0; attempt < 6; attempt++) {
const { data, error } = await supabase
.from('messages')
.select(
'id, conversation_id, sender_id, sender_device_id, reply_to_id, edited_at, deleted_at, created_at, ciphertext, nonce, key_version',
)
.eq('id', partial.id)
.maybeSingle();
if (error) {
console.warn('handleUpdate refetch failed', error);
return;
}
if (!data) {
await new Promise((r) => window.setTimeout(r, 120 * (attempt + 1)));
continue;
}
const r = data as unknown as {
id: string;
conversation_id: string;
sender_id: string;
sender_device_id: string | null;
reply_to_id: string | null;
edited_at: string | null;
deleted_at: string | null;
created_at: string;
ciphertext: string;
nonce: string;
key_version: number;
};
const msg: MessageWithCipher = {
id: r.id,
conversationId: r.conversation_id,
senderId: r.sender_id,
senderDeviceId: r.sender_device_id,
replyToId: r.reply_to_id,
editedAt: r.edited_at,
deletedAt: r.deleted_at,
createdAt: r.created_at,
ciphertext: pgBytesToBytes(String(r.ciphertext)),
nonce: pgBytesToBytes(String(r.nonce)),
keyVersion: r.key_version,
};
const [d] = await decryptBatch([msg]);
if (d) {
decrypted = d;
if (d.plaintext !== null) break;
}
await new Promise((r) => window.setTimeout(r, 200 * (attempt + 1)));
}
if (!decrypted) return;
setState((prev) => {
const idx = prev.messages.findIndex((m) => m.id === decrypted.id);
const idx = prev.messages.findIndex((m) => m.id === decrypted!.id);
if (idx === -1) return prev;
const next = [...prev.messages];
next[idx] = decrypted;
next[idx] = decrypted!;
return { ...prev, messages: next };
});
}
},
[decryptBatch],
[conversationId, deviceId, decryptBatch],
);
const handleDelete = useCallback((row: Record<string, unknown>) => {
@@ -256,7 +309,26 @@ export function useConversationMessages({ conversationId, userId, deviceId }: Ar
)
.subscribe();
// Refresh + reconnect on wake from background throttle (mostly Windows
// WebView2). Without this, messages inserted while the window is
// minimised never arrive until the user explicitly reloads.
const onAwake = () => {
if (document.visibilityState !== 'visible') return;
void refresh();
try {
channel.subscribe();
} catch {
/* already live */
}
};
document.addEventListener('visibilitychange', onAwake);
window.addEventListener('focus', onAwake);
window.addEventListener('online', onAwake);
return () => {
document.removeEventListener('visibilitychange', onAwake);
window.removeEventListener('focus', onAwake);
window.removeEventListener('online', onAwake);
void supabase.removeChannel(channel);
};
}, [conversationId, userId, deviceId, refresh, handleInsert, handleUpdate, handleDelete]);
+17
View File
@@ -44,7 +44,24 @@ export function useFriendships(userId: string | undefined): FriendshipsState & {
})
.subscribe();
// Windows WebView2 throttles background sockets — refresh on wake.
const onAwake = () => {
if (document.visibilityState !== 'visible') return;
void refresh();
try {
channel.subscribe();
} catch {
/* already live */
}
};
document.addEventListener('visibilitychange', onAwake);
window.addEventListener('focus', onAwake);
window.addEventListener('online', onAwake);
return () => {
document.removeEventListener('visibilitychange', onAwake);
window.removeEventListener('focus', onAwake);
window.removeEventListener('online', onAwake);
void supabase.removeChannel(channel);
};
}, [userId, refresh]);