diff --git a/src/app/components/message/content/ImageContent.tsx b/src/app/components/message/content/ImageContent.tsx index 47ece05e1..c085e8479 100644 --- a/src/app/components/message/content/ImageContent.tsx +++ b/src/app/components/message/content/ImageContent.tsx @@ -33,6 +33,7 @@ import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { ModalWide } from '../../../styles/Modal.css'; import { validBlurHash } from '../../../utils/blurHash'; import { useNearViewport } from '../../../hooks/useNearViewport'; +import { whenMediaAuthReady } from '../../../../sw-session'; type RenderViewerProps = { src: string; @@ -115,13 +116,24 @@ export const ImageContent = as<'div', ImageContentProps>( const handleLoad = () => { setLoad(true); }; + // An image requested before the service worker controls the page goes out + // without the access token and fails (first visit, just after an update). + // Retry once by itself when that's ready; only then offer "Retry". + const [attempt, setAttempt] = useState(0); + const autoRetried = useRef(false); const handleError = () => { setLoad(false); + if (!autoRetried.current && !encInfo) { + autoRetried.current = true; + whenMediaAuthReady().then(() => setAttempt((n) => n + 1)); + return; + } setError(true); }; const handleRetry = () => { setError(false); + setAttempt((n) => n + 1); loadSrc(); }; @@ -182,7 +194,7 @@ export const ImageContent = as<'div', ImageContentProps>( )} {srcState.status === AsyncStatus.Success && ( - + {renderImage({ alt: body, title: body, diff --git a/src/sw-session.ts b/src/sw-session.ts index e4d2672da..a094f45db 100644 --- a/src/sw-session.ts +++ b/src/sw-session.ts @@ -9,3 +9,25 @@ export function pushSessionToSW(baseUrl?: string, accessToken?: string, userId?: userId, }); } + +/** + * Resolves once media requests can carry the session: immediately-ish when a + * service worker already controls the page, otherwise when one takes control + * (first visit / just updated) or after `timeoutMs`. Used to retry a media + * load that raced the worker's activation and went out without a token. + */ +export function whenMediaAuthReady(timeoutMs = 3000): Promise { + return new Promise((resolve) => { + const sw = 'serviceWorker' in navigator ? navigator.serviceWorker : undefined; + if (!sw || sw.controller) { + window.setTimeout(resolve, 500); + return; + } + const done = () => { + sw.removeEventListener('controllerchange', done); + resolve(); + }; + sw.addEventListener('controllerchange', done); + window.setTimeout(done, timeoutMs); + }); +}