Compare commits
2 Commits
4756bfdc57
...
a0c0068997
| Author | SHA1 | Date | |
|---|---|---|---|
| a0c0068997 | |||
| c07cf58086 |
@@ -35,15 +35,23 @@ import { validBlurHash } from '../../../utils/blurHash';
|
|||||||
/**
|
/**
|
||||||
* Fetches media with authentication headers and returns a blob URL.
|
* Fetches media with authentication headers and returns a blob URL.
|
||||||
* This is needed because service workers don't work reliably in Tauri/WebKit.
|
* This is needed because service workers don't work reliably in Tauri/WebKit.
|
||||||
|
* Falls back to unauthenticated request if authenticated request fails with 401.
|
||||||
*/
|
*/
|
||||||
const fetchAuthenticatedMedia = async (
|
const fetchAuthenticatedMedia = async (
|
||||||
url: string,
|
url: string,
|
||||||
accessToken: string | null
|
accessToken: string | null
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
const response = await fetch(url, {
|
let response = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If we got a 401 and we tried with auth, fallback to unauthenticated request
|
||||||
|
if (!response.ok && response.status === 401 && accessToken) {
|
||||||
|
console.warn('[ImageContent] Auth failed (401), attempting unauthenticated fallback for:', url);
|
||||||
|
response = await fetch(url, { method: 'GET' });
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch media: ${response.status}`);
|
throw new Error(`Failed to fetch media: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,15 +8,23 @@ import { FALLBACK_MIMETYPE } from '../../../utils/mimeTypes';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches media with authentication headers and returns a blob URL.
|
* Fetches media with authentication headers and returns a blob URL.
|
||||||
|
* Falls back to unauthenticated request if authenticated request fails with 401.
|
||||||
*/
|
*/
|
||||||
const fetchAuthenticatedMedia = async (
|
const fetchAuthenticatedMedia = async (
|
||||||
url: string,
|
url: string,
|
||||||
accessToken: string | null
|
accessToken: string | null
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
const response = await fetch(url, {
|
let response = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If we got a 401 and we tried with auth, fallback to unauthenticated request
|
||||||
|
if (!response.ok && response.status === 401 && accessToken) {
|
||||||
|
console.warn('[ThumbnailContent] Auth failed (401), attempting unauthenticated fallback for:', url);
|
||||||
|
response = await fetch(url, { method: 'GET' });
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch media: ${response.status}`);
|
throw new Error(`Failed to fetch media: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { MouseEventHandler, forwardRef, useState } from 'react';
|
import React, { MouseEventHandler, forwardRef, useState, useMemo } from 'react';
|
||||||
import { Room, EventTimeline } from 'matrix-js-sdk';
|
import { Room, EventTimeline } from 'matrix-js-sdk';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import {
|
import {
|
||||||
@@ -283,18 +283,22 @@ export function RoomNavItem({
|
|||||||
const mDirects = useAtomValue(mDirectAtom);
|
const mDirects = useAtomValue(mDirectAtom);
|
||||||
|
|
||||||
// Get first participated thread for preview
|
// Get first participated thread for preview
|
||||||
const firstThreadPreview = (() => {
|
const firstThreadPreview = useMemo(() => {
|
||||||
const threads = room.getThreads();
|
try {
|
||||||
const participatedThreads = threads.filter((thread) => thread.hasCurrentUserParticipated);
|
const threads = room.getThreads();
|
||||||
if (participatedThreads.length === 0) return undefined;
|
const participatedThreads = threads.filter((thread) => thread.hasCurrentUserParticipated);
|
||||||
|
if (participatedThreads.length === 0) return undefined;
|
||||||
|
|
||||||
const firstThread = participatedThreads[0];
|
const firstThread = participatedThreads[0];
|
||||||
const rootEvent = firstThread.rootEvent;
|
const rootEvent = firstThread.rootEvent;
|
||||||
if (!rootEvent) return undefined;
|
if (!rootEvent) return undefined;
|
||||||
|
|
||||||
const rootBody = rootEvent.getContent()?.body ?? '';
|
const rootBody = rootEvent.getContent()?.body ?? '';
|
||||||
return rootBody;
|
return rootBody;
|
||||||
})();
|
} catch (error) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}, [room]);
|
||||||
|
|
||||||
// Get parent space for DMs
|
// Get parent space for DMs
|
||||||
const parentSpaceInfo = (() => {
|
const parentSpaceInfo = (() => {
|
||||||
|
|||||||
@@ -550,7 +550,7 @@ export function ThreadView({ room, threadRootId }: ThreadViewProps) {
|
|||||||
hour24Clock={hour24Clock}
|
hour24Clock={hour24Clock}
|
||||||
dateFormatString={dateFormatString}
|
dateFormatString={dateFormatString}
|
||||||
reply={
|
reply={
|
||||||
replyEventId ? (
|
replyEventId && replyEventId !== threadRootId ? (
|
||||||
<Reply
|
<Reply
|
||||||
room={room}
|
room={room}
|
||||||
timelineSet={roomTimelineSet}
|
timelineSet={roomTimelineSet}
|
||||||
|
|||||||
@@ -41,13 +41,19 @@ export const useAuthenticatedMediaUrl = (
|
|||||||
const fetchMedia = async () => {
|
const fetchMedia = async () => {
|
||||||
try {
|
try {
|
||||||
const accessToken = mx.getAccessToken();
|
const accessToken = mx.getAccessToken();
|
||||||
const response = await fetch(src, {
|
let response = await fetch(src, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: accessToken
|
headers: accessToken
|
||||||
? { Authorization: `Bearer ${accessToken}` }
|
? { Authorization: `Bearer ${accessToken}` }
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If we got a 401 and we tried with auth, fallback to unauthenticated request
|
||||||
|
if (!response.ok && response.status === 401 && accessToken) {
|
||||||
|
console.warn('[useAuthenticatedMediaUrl] Auth failed (401), attempting unauthenticated fallback for:', src);
|
||||||
|
response = await fetch(src, { method: 'GET' });
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.warn(`Failed to fetch authenticated media: ${response.status}`);
|
console.warn(`Failed to fetch authenticated media: ${response.status}`);
|
||||||
// Fall back to original URL in case server doesn't require auth
|
// Fall back to original URL in case server doesn't require auth
|
||||||
@@ -99,13 +105,19 @@ export const useAuthenticatedMediaFetch = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const accessToken = mx.getAccessToken();
|
const accessToken = mx.getAccessToken();
|
||||||
const response = await fetch(src, {
|
let response = await fetch(src, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: accessToken
|
headers: accessToken
|
||||||
? { Authorization: `Bearer ${accessToken}` }
|
? { Authorization: `Bearer ${accessToken}` }
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If we got a 401 and we tried with auth, fallback to unauthenticated request
|
||||||
|
if (!response.ok && response.status === 401 && accessToken) {
|
||||||
|
console.warn('[useAuthenticatedMediaFetch] Auth failed (401), attempting unauthenticated fallback');
|
||||||
|
response = await fetch(src, { method: 'GET' });
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.warn(`Failed to fetch authenticated media: ${response.status}`);
|
console.warn(`Failed to fetch authenticated media: ${response.status}`);
|
||||||
return src;
|
return src;
|
||||||
|
|||||||
@@ -26,10 +26,16 @@ async function fetchAvatarData(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const accessToken = mx.getAccessToken();
|
const accessToken = mx.getAccessToken();
|
||||||
const response = await fetch(url, {
|
let response = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: accessToken && useAuthentication ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
headers: accessToken && useAuthentication ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If we got a 401 and we tried with auth, fallback to unauthenticated request
|
||||||
|
if (!response.ok && response.status === 401 && accessToken && useAuthentication) {
|
||||||
|
response = await fetch(url, { method: 'GET' });
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) return null;
|
if (!response.ok) return null;
|
||||||
return await response.arrayBuffer();
|
return await response.arrayBuffer();
|
||||||
} catch {
|
} catch {
|
||||||
@@ -112,8 +118,6 @@ export function useUserBanner(): [
|
|||||||
throw new Error('No user ID');
|
throw new Error('No user ID');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[useUserBanner] Starting banner update:', newBanner);
|
|
||||||
|
|
||||||
const profile = await mx.getProfileInfo(userId);
|
const profile = await mx.getProfileInfo(userId);
|
||||||
const avatarUrl = profile.avatar_url;
|
const avatarUrl = profile.avatar_url;
|
||||||
|
|
||||||
@@ -121,19 +125,14 @@ export function useUserBanner(): [
|
|||||||
throw new Error('No avatar set. Please upload an avatar first.');
|
throw new Error('No avatar set. Please upload an avatar first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[useUserBanner] Current avatar URL:', avatarUrl);
|
|
||||||
|
|
||||||
// Fetch current avatar
|
// Fetch current avatar
|
||||||
const avatarData = await fetchAvatarData(mx, avatarUrl, useAuthentication);
|
const avatarData = await fetchAvatarData(mx, avatarUrl, useAuthentication);
|
||||||
if (!avatarData) {
|
if (!avatarData) {
|
||||||
throw new Error('Failed to fetch current avatar');
|
throw new Error('Failed to fetch current avatar');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[useUserBanner] Fetched avatar data, size:', avatarData.byteLength);
|
|
||||||
|
|
||||||
// Detect image format
|
// Detect image format
|
||||||
const format = detectImageFormat(avatarData);
|
const format = detectImageFormat(avatarData);
|
||||||
console.log('[useUserBanner] Detected format:', format);
|
|
||||||
|
|
||||||
if (format === 'unknown') {
|
if (format === 'unknown') {
|
||||||
throw new Error('Unsupported avatar image format');
|
throw new Error('Unsupported avatar image format');
|
||||||
@@ -141,7 +140,6 @@ export function useUserBanner(): [
|
|||||||
|
|
||||||
// Get existing metadata to preserve
|
// Get existing metadata to preserve
|
||||||
const existingMetadata = extractMetadataFromImage(avatarData);
|
const existingMetadata = extractMetadataFromImage(avatarData);
|
||||||
console.log('[useUserBanner] Existing metadata:', existingMetadata);
|
|
||||||
|
|
||||||
// Modify image metadata with new banner, preserving color
|
// Modify image metadata with new banner, preserving color
|
||||||
const newMetadata: ImageMetadata = {
|
const newMetadata: ImageMetadata = {
|
||||||
@@ -149,18 +147,14 @@ export function useUserBanner(): [
|
|||||||
banner: newBanner,
|
banner: newBanner,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('[useUserBanner] Embedding metadata:', newMetadata);
|
|
||||||
const newAvatarData = embedMetadataInImage(avatarData, newMetadata);
|
const newAvatarData = embedMetadataInImage(avatarData, newMetadata);
|
||||||
|
|
||||||
if (!newAvatarData) {
|
if (!newAvatarData) {
|
||||||
throw new Error('Failed to embed banner in avatar metadata');
|
throw new Error('Failed to embed banner in avatar metadata');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[useUserBanner] New avatar data size:', newAvatarData.byteLength);
|
|
||||||
|
|
||||||
// Verify the banner was embedded correctly before uploading
|
// Verify the banner was embedded correctly before uploading
|
||||||
const verifyMetadata = extractMetadataFromImage(newAvatarData);
|
const verifyMetadata = extractMetadataFromImage(newAvatarData);
|
||||||
console.log('[useUserBanner] Verification metadata:', verifyMetadata);
|
|
||||||
|
|
||||||
if (newBanner && verifyMetadata.banner !== newBanner) {
|
if (newBanner && verifyMetadata.banner !== newBanner) {
|
||||||
throw new Error('Banner verification failed');
|
throw new Error('Banner verification failed');
|
||||||
@@ -171,22 +165,19 @@ export function useUserBanner(): [
|
|||||||
const extension = getExtension(format);
|
const extension = getExtension(format);
|
||||||
const blob = new Blob([newAvatarData], { type: mimeType });
|
const blob = new Blob([newAvatarData], { type: mimeType });
|
||||||
|
|
||||||
console.log('[useUserBanner] Uploading avatar blob:', blob.size, 'bytes, type:', mimeType);
|
|
||||||
const uploadResponse = await mx.uploadContent(blob, {
|
const uploadResponse = await mx.uploadContent(blob, {
|
||||||
name: `avatar.${extension}`,
|
name: `avatar.${extension}`,
|
||||||
type: mimeType,
|
type: mimeType,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[useUserBanner] Upload response:', uploadResponse.content_uri);
|
|
||||||
|
|
||||||
// Update profile with new avatar
|
// Update profile with new avatar
|
||||||
console.log('[useUserBanner] Calling setAvatarUrl...');
|
|
||||||
try {
|
try {
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
mx.setAvatarUrl(uploadResponse.content_uri),
|
mx.setAvatarUrl(uploadResponse.content_uri),
|
||||||
new Promise((_, reject) => setTimeout(() => reject(new Error('setAvatarUrl timeout')), 30000))
|
new Promise((_, reject) => {
|
||||||
|
setTimeout(() => reject(new Error('setAvatarUrl timeout')), 30000);
|
||||||
|
})
|
||||||
]);
|
]);
|
||||||
console.log('[useUserBanner] Avatar URL updated successfully');
|
|
||||||
|
|
||||||
// Manually sync user object to ensure event listeners are triggered
|
// Manually sync user object to ensure event listeners are triggered
|
||||||
const user = mx.getUser(userId);
|
const user = mx.getUser(userId);
|
||||||
@@ -194,7 +185,6 @@ export function useUserBanner(): [
|
|||||||
user.setAvatarUrl(uploadResponse.content_uri);
|
user.setAvatarUrl(uploadResponse.content_uri);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[useUserBanner] setAvatarUrl failed:', error);
|
|
||||||
throw new Error(`Failed to update avatar URL: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
throw new Error(`Failed to update avatar URL: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +215,7 @@ export function useOtherUserBanner(userId: string, avatarMxc: string | undefined
|
|||||||
|
|
||||||
if (!avatarMxc) {
|
if (!avatarMxc) {
|
||||||
setBannerBlobUrl(undefined);
|
setBannerBlobUrl(undefined);
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadBanner = async () => {
|
const loadBanner = async () => {
|
||||||
@@ -275,7 +265,6 @@ export function useOtherUserBanner(userId: string, avatarMxc: string | undefined
|
|||||||
|
|
||||||
const response = await fetch(bannerHttpUrl, { headers });
|
const response = await fetch(bannerHttpUrl, { headers });
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.warn('Failed to fetch banner image:', response.status);
|
|
||||||
setBannerBlobUrl(undefined);
|
setBannerBlobUrl(undefined);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,10 +26,17 @@ async function fetchAvatarData(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const accessToken = mx.getAccessToken();
|
const accessToken = mx.getAccessToken();
|
||||||
const response = await fetch(url, {
|
let response = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: accessToken && useAuthentication ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
headers: accessToken && useAuthentication ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If we got a 401 and we tried with auth, fallback to unauthenticated request
|
||||||
|
if (!response.ok && response.status === 401 && accessToken && useAuthentication) {
|
||||||
|
console.warn('[fetchAvatarData] Auth failed (401), attempting unauthenticated fallback');
|
||||||
|
response = await fetch(url, { method: 'GET' });
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) return null;
|
if (!response.ok) return null;
|
||||||
return await response.arrayBuffer();
|
return await response.arrayBuffer();
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -27,10 +27,16 @@ async function fetchAvatarData(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const accessToken = mx.getAccessToken();
|
const accessToken = mx.getAccessToken();
|
||||||
const response = await fetch(url, {
|
let response = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: accessToken && useAuthentication ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
headers: accessToken && useAuthentication ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If we got a 401 and we tried with auth, fallback to unauthenticated request
|
||||||
|
if (!response.ok && response.status === 401 && accessToken && useAuthentication) {
|
||||||
|
response = await fetch(url, { method: 'GET' });
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) return null;
|
if (!response.ok) return null;
|
||||||
return await response.arrayBuffer();
|
return await response.arrayBuffer();
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -306,6 +306,7 @@ export const isAuthenticatedMediaUrl = (url: string): boolean =>
|
|||||||
/**
|
/**
|
||||||
* Downloads media with optional authentication.
|
* Downloads media with optional authentication.
|
||||||
* For authenticated media URLs, the access token is required.
|
* For authenticated media URLs, the access token is required.
|
||||||
|
* Falls back to unauthenticated request if authenticated request fails with 401.
|
||||||
*/
|
*/
|
||||||
export const downloadMedia = async (src: string, accessToken?: string | null): Promise<Blob> => {
|
export const downloadMedia = async (src: string, accessToken?: string | null): Promise<Blob> => {
|
||||||
const needsAuth = isAuthenticatedMediaUrl(src);
|
const needsAuth = isAuthenticatedMediaUrl(src);
|
||||||
@@ -315,7 +316,14 @@ export const downloadMedia = async (src: string, accessToken?: string | null): P
|
|||||||
headers.Authorization = `Bearer ${accessToken}`;
|
headers.Authorization = `Bearer ${accessToken}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(src, { method: 'GET', headers });
|
let res = await fetch(src, { method: 'GET', headers });
|
||||||
|
|
||||||
|
// If we got a 401 and we tried with auth, fallback to unauthenticated request
|
||||||
|
if (!res.ok && res.status === 401 && needsAuth && accessToken) {
|
||||||
|
console.warn('[downloadMedia] Auth failed (401), attempting unauthenticated fallback');
|
||||||
|
res = await fetch(src, { method: 'GET' });
|
||||||
|
}
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`Failed to download media: ${res.status}`);
|
throw new Error(`Failed to download media: ${res.status}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import { cryptoCallbacks } from './secretStorageKeys';
|
|||||||
import { clearNavToActivePathStore } from '../app/state/navToActivePath';
|
import { clearNavToActivePathStore } from '../app/state/navToActivePath';
|
||||||
import { Session, getSessionStoreName } from '../app/state/sessions';
|
import { Session, getSessionStoreName } from '../app/state/sessions';
|
||||||
|
|
||||||
|
// Track if we're currently handling a key conflict to prevent loops
|
||||||
|
let handlingKeyConflict = false;
|
||||||
|
|
||||||
const deleteIndexedDB = (dbName: string): Promise<void> => {
|
const deleteIndexedDB = (dbName: string): Promise<void> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = global.indexedDB.deleteDatabase(dbName);
|
const request = global.indexedDB.deleteDatabase(dbName);
|
||||||
@@ -73,9 +76,129 @@ const deleteAllMatrixDatabases = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<boolean> => {
|
||||||
|
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<MatrixClient> => {
|
export const initClient = async (session: Session): Promise<MatrixClient> => {
|
||||||
const storeName = getSessionStoreName(session);
|
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] ========================================');
|
||||||
console.log('[initMatrix] Initializing Matrix client for session:');
|
console.log('[initMatrix] Initializing Matrix client for session:');
|
||||||
console.log('[initMatrix] User ID:', session.userId);
|
console.log('[initMatrix] User ID:', session.userId);
|
||||||
@@ -153,6 +276,9 @@ export const initClient = async (session: Session): Promise<MatrixClient> => {
|
|||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Setup handler to detect and auto-fix one-time key conflicts
|
||||||
|
setupKeyConflictHandler(mx, session);
|
||||||
|
|
||||||
return mx;
|
return mx;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Handle crypto store account mismatch error
|
// Handle crypto store account mismatch error
|
||||||
@@ -188,6 +314,51 @@ export const clearCacheAndReload = async (mx: MatrixClient) => {
|
|||||||
window.location.reload();
|
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<void> => {
|
||||||
|
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) => {
|
export const logoutClient = async (mx: MatrixClient) => {
|
||||||
mx.stopClient();
|
mx.stopClient();
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user