fix: editable decrypt + windows realtime wake (v0.7.1)
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:
@@ -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]);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user