diff --git a/src/app/features/room/ThreadView.tsx b/src/app/features/room/ThreadView.tsx index b891883..5f6b26a 100644 --- a/src/app/features/room/ThreadView.tsx +++ b/src/app/features/room/ThreadView.tsx @@ -550,7 +550,7 @@ export function ThreadView({ room, threadRootId }: ThreadViewProps) { hour24Clock={hour24Clock} dateFormatString={dateFormatString} reply={ - replyEventId ? ( + replyEventId && replyEventId !== threadRootId ? ( => { return new Promise((resolve, reject) => { const request = global.indexedDB.deleteDatabase(dbName); @@ -73,9 +76,129 @@ const deleteAllMatrixDatabases = async (): Promise => { } }; +/** + * Delete the Rust crypto database to fix one-time key conflicts. + * This happens when the local crypto state has keys that conflict with server state. + */ +const deleteRustCryptoDatabase = async (session: Session): Promise => { + console.log('[initMatrix] 🔑 Attempting to delete Rust crypto database to fix key conflicts...'); + + const dbNames = await getAllIndexedDBNames(); + const storagePrefix = `${session.userId}@${session.deviceId}`; + + // Find crypto databases for this session + const cryptoDbNames = dbNames.filter(name => + name.includes('matrix-sdk-crypto') || + name.includes(storagePrefix) + ); + + if (cryptoDbNames.length === 0) { + console.log('[initMatrix] No crypto databases found to delete'); + return false; + } + + console.log('[initMatrix] 🗑️ Deleting crypto databases:', cryptoDbNames); + + for (const dbName of cryptoDbNames) { + try { + await deleteIndexedDB(dbName); + console.log(`[initMatrix] ✅ Deleted: ${dbName}`); + } catch (error) { + console.error(`[initMatrix] ❌ Failed to delete ${dbName}:`, error); + } + } + + return true; +}; + +/** + * Setup error listener to detect one-time key conflicts and auto-fix them. + */ +const setupKeyConflictHandler = (mx: MatrixClient, session: Session): void => { + // Listen for HTTP errors from the client + let keyConflictCount = 0; + const KEY_CONFLICT_THRESHOLD = 3; + + // Patch console.log/error to detect the key conflict errors + const originalConsoleLog = console.log; + const keyConflictPattern = /One time key.*already exists/; + + // We need to use a different approach - listen to the crypto module + // Since we can't directly intercept, we'll use a MutationObserver-style approach + // by checking for repeated key upload failures + + const checkKeyConflict = (args: any[]): boolean => { + const message = args.map(a => String(a)).join(' '); + return keyConflictPattern.test(message); + }; + + // Override console methods temporarily to detect the errors + const originalError = console.error; + const originalWarn = console.warn; + + const errorHandler = (...args: any[]) => { + if (checkKeyConflict(args)) { + keyConflictCount++; + console.log(`[initMatrix] ⚠️ Detected key conflict (${keyConflictCount}/${KEY_CONFLICT_THRESHOLD})`); + + if (keyConflictCount >= KEY_CONFLICT_THRESHOLD && !handlingKeyConflict) { + handlingKeyConflict = true; + console.log('[initMatrix] 🔄 Too many key conflicts detected, attempting auto-fix...'); + + // Schedule the fix for next page load to avoid issues during active session + localStorage.setItem('cinny_fix_key_conflict', JSON.stringify({ + userId: session.userId, + deviceId: session.deviceId, + timestamp: Date.now() + })); + + // Restore console methods + console.error = originalError; + console.warn = originalWarn; + + // Notify user and reload + setTimeout(() => { + if (confirm('Detected encryption key conflict. The app needs to reset your encryption session. You may need to re-verify your devices after this. Continue?')) { + deleteRustCryptoDatabase(session).then(() => { + window.location.reload(); + }); + } else { + handlingKeyConflict = false; + } + }, 100); + } + } + originalError.apply(console, args); + }; + + console.error = errorHandler; + console.warn = (...args: any[]) => { + if (checkKeyConflict(args)) { + keyConflictCount++; + } + originalWarn.apply(console, args); + }; +}; + export const initClient = async (session: Session): Promise => { const storeName = getSessionStoreName(session); + // Check if we need to fix a key conflict from a previous session + const pendingFix = localStorage.getItem('cinny_fix_key_conflict'); + if (pendingFix) { + try { + const fixData = JSON.parse(pendingFix); + if (fixData.userId === session.userId && fixData.deviceId === session.deviceId) { + console.log('[initMatrix] 🔧 Found pending key conflict fix, clearing crypto database...'); + await deleteRustCryptoDatabase(session); + localStorage.removeItem('cinny_fix_key_conflict'); + console.log('[initMatrix] ✅ Crypto database cleared'); + } + } catch (e) { + localStorage.removeItem('cinny_fix_key_conflict'); + } + } + console.log('[initMatrix] ========================================'); console.log('[initMatrix] Initializing Matrix client for session:'); console.log('[initMatrix] User ID:', session.userId); @@ -153,6 +276,9 @@ export const initClient = async (session: Session): Promise => { return user; }; + // Setup handler to detect and auto-fix one-time key conflicts + setupKeyConflictHandler(mx, session); + return mx; } catch (error: any) { // Handle crypto store account mismatch error @@ -188,6 +314,51 @@ export const clearCacheAndReload = async (mx: MatrixClient) => { window.location.reload(); }; +/** + * Fix one-time key conflicts by clearing the Rust crypto database. + * Call this when you see repeated "One time key already exists" errors. + * The user will need to re-verify their devices after this. + */ +export const fixKeyConflict = async (mx: MatrixClient): Promise => { + const userId = mx.getSafeUserId(); + const deviceId = mx.getDeviceId(); + + if (!userId || !deviceId) { + console.error('[initMatrix] Cannot fix key conflict: missing userId or deviceId'); + return; + } + + console.log('[initMatrix] 🔧 Fixing key conflict for', userId, deviceId); + + // Stop the client first + mx.stopClient(); + + // Delete the crypto databases + const dbNames = await getAllIndexedDBNames(); + const storagePrefix = `${userId}@${deviceId}`; + + const cryptoDbNames = dbNames.filter(name => + name.includes('matrix-sdk-crypto') || + name.includes(storagePrefix) || + name.includes('crypto') + ); + + console.log('[initMatrix] 🗑️ Deleting crypto databases:', cryptoDbNames); + + for (const dbName of cryptoDbNames) { + try { + await deleteIndexedDB(dbName); + console.log(`[initMatrix] ✅ Deleted: ${dbName}`); + } catch (error) { + console.error(`[initMatrix] ❌ Failed to delete ${dbName}:`, error); + } + } + + // Reload the page + console.log('[initMatrix] 🔄 Reloading...'); + window.location.reload(); +}; + export const logoutClient = async (mx: MatrixClient) => { mx.stopClient(); try {