diff --git a/src/app/components/message/content/ImageContent.tsx b/src/app/components/message/content/ImageContent.tsx index d952723..831f71e 100644 --- a/src/app/components/message/content/ImageContent.tsx +++ b/src/app/components/message/content/ImageContent.tsx @@ -35,15 +35,23 @@ import { validBlurHash } from '../../../utils/blurHash'; /** * Fetches media with authentication headers and returns a blob URL. * 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 ( url: string, accessToken: string | null ): Promise => { - const response = await fetch(url, { + let response = await fetch(url, { method: 'GET', 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) { throw new Error(`Failed to fetch media: ${response.status}`); } diff --git a/src/app/components/message/content/ThumbnailContent.tsx b/src/app/components/message/content/ThumbnailContent.tsx index 74d015d..15520de 100644 --- a/src/app/components/message/content/ThumbnailContent.tsx +++ b/src/app/components/message/content/ThumbnailContent.tsx @@ -8,15 +8,23 @@ import { FALLBACK_MIMETYPE } from '../../../utils/mimeTypes'; /** * Fetches media with authentication headers and returns a blob URL. + * Falls back to unauthenticated request if authenticated request fails with 401. */ const fetchAuthenticatedMedia = async ( url: string, accessToken: string | null ): Promise => { - const response = await fetch(url, { + let response = await fetch(url, { method: 'GET', 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) { throw new Error(`Failed to fetch media: ${response.status}`); } diff --git a/src/app/features/room-nav/RoomNavItem.tsx b/src/app/features/room-nav/RoomNavItem.tsx index 9ca77dc..f59d340 100644 --- a/src/app/features/room-nav/RoomNavItem.tsx +++ b/src/app/features/room-nav/RoomNavItem.tsx @@ -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 { useAtomValue } from 'jotai'; import { @@ -283,18 +283,22 @@ export function RoomNavItem({ const mDirects = useAtomValue(mDirectAtom); // Get first participated thread for preview - const firstThreadPreview = (() => { - const threads = room.getThreads(); - const participatedThreads = threads.filter((thread) => thread.hasCurrentUserParticipated); - if (participatedThreads.length === 0) return undefined; + const firstThreadPreview = useMemo(() => { + try { + const threads = room.getThreads(); + const participatedThreads = threads.filter((thread) => thread.hasCurrentUserParticipated); + if (participatedThreads.length === 0) return undefined; - const firstThread = participatedThreads[0]; - const rootEvent = firstThread.rootEvent; - if (!rootEvent) return undefined; + const firstThread = participatedThreads[0]; + const rootEvent = firstThread.rootEvent; + if (!rootEvent) return undefined; - const rootBody = rootEvent.getContent()?.body ?? ''; - return rootBody; - })(); + const rootBody = rootEvent.getContent()?.body ?? ''; + return rootBody; + } catch (error) { + return undefined; + } + }, [room]); // Get parent space for DMs const parentSpaceInfo = (() => { diff --git a/src/app/hooks/useAuthenticatedMediaUrl.ts b/src/app/hooks/useAuthenticatedMediaUrl.ts index 2e2d601..f4c10d6 100644 --- a/src/app/hooks/useAuthenticatedMediaUrl.ts +++ b/src/app/hooks/useAuthenticatedMediaUrl.ts @@ -41,13 +41,19 @@ export const useAuthenticatedMediaUrl = ( const fetchMedia = async () => { try { const accessToken = mx.getAccessToken(); - const response = await fetch(src, { + let response = await fetch(src, { method: 'GET', 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('[useAuthenticatedMediaUrl] Auth failed (401), attempting unauthenticated fallback for:', src); + response = await fetch(src, { method: 'GET' }); + } + if (!response.ok) { console.warn(`Failed to fetch authenticated media: ${response.status}`); // Fall back to original URL in case server doesn't require auth @@ -99,13 +105,19 @@ export const useAuthenticatedMediaFetch = () => { try { const accessToken = mx.getAccessToken(); - const response = await fetch(src, { + let response = await fetch(src, { method: 'GET', 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('[useAuthenticatedMediaFetch] Auth failed (401), attempting unauthenticated fallback'); + response = await fetch(src, { method: 'GET' }); + } + if (!response.ok) { console.warn(`Failed to fetch authenticated media: ${response.status}`); return src; diff --git a/src/app/hooks/useUserBanner.ts b/src/app/hooks/useUserBanner.ts index b7d4e3d..067c4de 100644 --- a/src/app/hooks/useUserBanner.ts +++ b/src/app/hooks/useUserBanner.ts @@ -26,10 +26,16 @@ async function fetchAvatarData( try { const accessToken = mx.getAccessToken(); - const response = await fetch(url, { + let response = await fetch(url, { method: 'GET', 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; return await response.arrayBuffer(); } catch { @@ -112,8 +118,6 @@ export function useUserBanner(): [ throw new Error('No user ID'); } - console.log('[useUserBanner] Starting banner update:', newBanner); - const profile = await mx.getProfileInfo(userId); const avatarUrl = profile.avatar_url; @@ -121,19 +125,14 @@ export function useUserBanner(): [ throw new Error('No avatar set. Please upload an avatar first.'); } - console.log('[useUserBanner] Current avatar URL:', avatarUrl); - // Fetch current avatar const avatarData = await fetchAvatarData(mx, avatarUrl, useAuthentication); if (!avatarData) { throw new Error('Failed to fetch current avatar'); } - console.log('[useUserBanner] Fetched avatar data, size:', avatarData.byteLength); - // Detect image format const format = detectImageFormat(avatarData); - console.log('[useUserBanner] Detected format:', format); if (format === 'unknown') { throw new Error('Unsupported avatar image format'); @@ -141,7 +140,6 @@ export function useUserBanner(): [ // Get existing metadata to preserve const existingMetadata = extractMetadataFromImage(avatarData); - console.log('[useUserBanner] Existing metadata:', existingMetadata); // Modify image metadata with new banner, preserving color const newMetadata: ImageMetadata = { @@ -149,18 +147,14 @@ export function useUserBanner(): [ banner: newBanner, }; - console.log('[useUserBanner] Embedding metadata:', newMetadata); const newAvatarData = embedMetadataInImage(avatarData, newMetadata); if (!newAvatarData) { 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 const verifyMetadata = extractMetadataFromImage(newAvatarData); - console.log('[useUserBanner] Verification metadata:', verifyMetadata); if (newBanner && verifyMetadata.banner !== newBanner) { throw new Error('Banner verification failed'); @@ -171,22 +165,19 @@ export function useUserBanner(): [ const extension = getExtension(format); const blob = new Blob([newAvatarData], { type: mimeType }); - console.log('[useUserBanner] Uploading avatar blob:', blob.size, 'bytes, type:', mimeType); const uploadResponse = await mx.uploadContent(blob, { name: `avatar.${extension}`, type: mimeType, }); - console.log('[useUserBanner] Upload response:', uploadResponse.content_uri); - // Update profile with new avatar - console.log('[useUserBanner] Calling setAvatarUrl...'); try { await Promise.race([ 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 const user = mx.getUser(userId); @@ -194,7 +185,6 @@ export function useUserBanner(): [ user.setAvatarUrl(uploadResponse.content_uri); } } catch (error) { - console.error('[useUserBanner] setAvatarUrl failed:', 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) { setBannerBlobUrl(undefined); - return; + return undefined; } const loadBanner = async () => { @@ -275,7 +265,6 @@ export function useOtherUserBanner(userId: string, avatarMxc: string | undefined const response = await fetch(bannerHttpUrl, { headers }); if (!response.ok) { - console.warn('Failed to fetch banner image:', response.status); setBannerBlobUrl(undefined); return; } diff --git a/src/app/hooks/useUserColor.ts b/src/app/hooks/useUserColor.ts index ed1b8c7..ee7c964 100644 --- a/src/app/hooks/useUserColor.ts +++ b/src/app/hooks/useUserColor.ts @@ -26,10 +26,17 @@ async function fetchAvatarData( try { const accessToken = mx.getAccessToken(); - const response = await fetch(url, { + let response = await fetch(url, { method: 'GET', 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; return await response.arrayBuffer(); } catch { diff --git a/src/app/hooks/useUserProfileStyle.ts b/src/app/hooks/useUserProfileStyle.ts index 47f0516..c8a50e7 100644 --- a/src/app/hooks/useUserProfileStyle.ts +++ b/src/app/hooks/useUserProfileStyle.ts @@ -27,10 +27,16 @@ async function fetchAvatarData( try { const accessToken = mx.getAccessToken(); - const response = await fetch(url, { + let response = await fetch(url, { method: 'GET', 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; return await response.arrayBuffer(); } catch { diff --git a/src/app/utils/matrix.ts b/src/app/utils/matrix.ts index 4bc6c51..d5ef76c 100644 --- a/src/app/utils/matrix.ts +++ b/src/app/utils/matrix.ts @@ -306,6 +306,7 @@ export const isAuthenticatedMediaUrl = (url: string): boolean => /** * Downloads media with optional authentication. * 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 => { const needsAuth = isAuthenticatedMediaUrl(src); @@ -315,7 +316,14 @@ export const downloadMedia = async (src: string, accessToken?: string | null): P 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) { throw new Error(`Failed to download media: ${res.status}`); }