Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ff5ea274b9 | |||
| b0f9f1dada | |||
| 961ac2dde5 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "ChatApp",
|
"productName": "ChatApp",
|
||||||
"version": "0.4.1",
|
"version": "0.4.4",
|
||||||
"identifier": "com.meinname.chatapp",
|
"identifier": "com.meinname.chatapp",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm vite:dev",
|
"beforeDevCommand": "pnpm vite:dev",
|
||||||
|
|||||||
@@ -164,9 +164,12 @@ async function syncOneConversationGaps(ctx: SyncCtx, convId: string): Promise<vo
|
|||||||
try {
|
try {
|
||||||
await shareConvKeyToDevice(supabase, convId, dev.id, pgHexToBytes(dev.public_key), ownCtx);
|
await shareConvKeyToDevice(supabase, convId, dev.id, pgHexToBytes(dev.public_key), ownCtx);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
// Most common: this device hasn't been wrapped for us yet either, so
|
// Backfill is best-effort. Most common silent failures:
|
||||||
// tryGetConvKey couldn't unwrap. Another peer with the key will fill
|
// - tryGetConvKey couldn't unwrap (another peer will fill the gap).
|
||||||
// the gap when they hit syncAllExistingGaps.
|
// - RLS rejects because the recipient's owner is a pending (not-yet
|
||||||
|
// accepted) DM member, or was removed from the conv.
|
||||||
|
// Both are recoverable / expected, swallow without spam.
|
||||||
|
if (isExpectedShareFailure(err)) continue;
|
||||||
console.warn('keySync: shareConvKeyToDevice gap-fill failed', {
|
console.warn('keySync: shareConvKeyToDevice gap-fill failed', {
|
||||||
convId,
|
convId,
|
||||||
recipient: dev.id,
|
recipient: dev.id,
|
||||||
@@ -176,6 +179,22 @@ async function syncOneConversationGaps(ctx: SyncCtx, convId: string): Promise<vo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isExpectedShareFailure(err: unknown): boolean {
|
||||||
|
if (!err || typeof err !== 'object') return false;
|
||||||
|
const e = err as { message?: string; code?: string; details?: string; status?: number };
|
||||||
|
const code = (e.code ?? '').toString();
|
||||||
|
const status = e.status;
|
||||||
|
const haystack = (e.message ?? '') + ' ' + (e.details ?? '');
|
||||||
|
return (
|
||||||
|
status === 403 ||
|
||||||
|
code === '42501' || // postgres: insufficient_privilege (RLS)
|
||||||
|
code === '23505' || // unique_violation
|
||||||
|
haystack.includes('row-level security') ||
|
||||||
|
haystack.includes('does not have it yet') ||
|
||||||
|
haystack.includes('Forbidden')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function wrapForOneDevice(
|
async function wrapForOneDevice(
|
||||||
ctx: SyncCtx,
|
ctx: SyncCtx,
|
||||||
newDeviceId: string,
|
newDeviceId: string,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import {
|
|||||||
sendNotification,
|
sendNotification,
|
||||||
} from '@tauri-apps/plugin-notification';
|
} from '@tauri-apps/plugin-notification';
|
||||||
|
|
||||||
|
import { isTauriRuntime } from './globalShortcut';
|
||||||
|
|
||||||
// Tracks whether permission has already been requested this session so we
|
// Tracks whether permission has already been requested this session so we
|
||||||
// don't spam the OS prompt. Actual permission state lives in the OS.
|
// don't spam the OS prompt. Actual permission state lives in the OS.
|
||||||
let permissionChecked = false;
|
let permissionChecked = false;
|
||||||
@@ -12,6 +14,11 @@ let permissionGranted = false;
|
|||||||
export async function ensureNotificationPermission(): Promise<boolean> {
|
export async function ensureNotificationPermission(): Promise<boolean> {
|
||||||
if (permissionChecked) return permissionGranted;
|
if (permissionChecked) return permissionGranted;
|
||||||
permissionChecked = true;
|
permissionChecked = true;
|
||||||
|
if (!isTauriRuntime()) {
|
||||||
|
// Web preview / Chrome — Tauri notification plugin not available.
|
||||||
|
permissionGranted = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
let granted = await isPermissionGranted();
|
let granted = await isPermissionGranted();
|
||||||
if (!granted) {
|
if (!granted) {
|
||||||
@@ -20,7 +27,6 @@ export async function ensureNotificationPermission(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
permissionGranted = granted;
|
permissionGranted = granted;
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
// Not running under Tauri (e.g. web preview) — fall back silently.
|
|
||||||
permissionGranted = false;
|
permissionGranted = false;
|
||||||
console.warn('notification permission check failed', err);
|
console.warn('notification permission check failed', err);
|
||||||
}
|
}
|
||||||
@@ -40,6 +46,7 @@ interface NotifyOpts {
|
|||||||
|
|
||||||
export async function notify({ title, body, force = false }: NotifyOpts): Promise<void> {
|
export async function notify({ title, body, force = false }: NotifyOpts): Promise<void> {
|
||||||
if (!force && isAppFocused()) return;
|
if (!force && isAppFocused()) return;
|
||||||
|
if (!isTauriRuntime()) return;
|
||||||
const granted = await ensureNotificationPermission();
|
const granted = await ensureNotificationPermission();
|
||||||
if (!granted) return;
|
if (!granted) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -161,7 +161,10 @@ export async function bootstrapConvKey(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { error } = await rawFrom(client, 'conversation_keys').insert(rows);
|
const { error } = await rawFrom(client, 'conversation_keys').upsert(rows, {
|
||||||
|
onConflict: 'conversation_id,recipient_device_id,key_version',
|
||||||
|
ignoreDuplicates: true,
|
||||||
|
});
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
const handle = { conversationId, keyVersion, key: convKey };
|
const handle = { conversationId, keyVersion, key: convKey };
|
||||||
@@ -262,15 +265,21 @@ export async function shareConvKeyToDevice(
|
|||||||
recipientPublicKey,
|
recipientPublicKey,
|
||||||
own.privateKey,
|
own.privateKey,
|
||||||
);
|
);
|
||||||
const { error } = await rawFrom(client, 'conversation_keys').insert({
|
const { error } = await rawFrom(client, 'conversation_keys').upsert(
|
||||||
|
{
|
||||||
conversation_id: conversationId,
|
conversation_id: conversationId,
|
||||||
recipient_device_id: recipientDeviceId,
|
recipient_device_id: recipientDeviceId,
|
||||||
key_version: version,
|
key_version: version,
|
||||||
sender_device_id: own.deviceId,
|
sender_device_id: own.deviceId,
|
||||||
encrypted_key: bytesToPgHex(wrapped.ciphertext),
|
encrypted_key: bytesToPgHex(wrapped.ciphertext),
|
||||||
nonce: bytesToPgHex(wrapped.nonce),
|
nonce: bytesToPgHex(wrapped.nonce),
|
||||||
});
|
},
|
||||||
if (error && !String(error.message ?? '').includes('duplicate')) throw error;
|
{
|
||||||
|
onConflict: 'conversation_id,recipient_device_id,key_version',
|
||||||
|
ignoreDuplicates: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (error) throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-exports for convenience.
|
// Re-exports for convenience.
|
||||||
|
|||||||
Reference in New Issue
Block a user