Files
cinny/src/app/utils/authenticatedMediaCache.ts
litruv c286501be8 Improve timeline scroll, media layout, avatars, and emoji coverage.
Remember room scroll position across switches, reserve image/video heights from Matrix dimensions, cache authenticated media blobs for avatars, restore jumbo emoji-only messages, and extend emoji font unicode-range for Unicode 15 glyphs.
2026-07-22 20:00:14 +10:00

88 lines
2.4 KiB
TypeScript

import { getCurrentAccessToken } from './auth';
const MAX_ENTRIES = 250;
/** Shared blob URLs for authenticated Matrix media (avatars, thumbs, etc.). */
const blobCache = new Map<string, string>();
const inflight = new Map<string, Promise<string>>();
export const isAuthenticatedMediaUrl = (url: string): boolean =>
url.includes('/_matrix/client/v1/media/download') ||
url.includes('/_matrix/client/v1/media/thumbnail') ||
(url.includes('/_matrix/media/') &&
(url.includes('/download/') || url.includes('/thumbnail/')));
export const getCachedAuthenticatedMediaUrl = (src: string | undefined): string | undefined => {
if (!src) return undefined;
return blobCache.get(src);
};
const touch = (src: string, blobUrl: string) => {
// Re-insert for LRU ordering (Map preserves insertion order).
if (blobCache.has(src)) blobCache.delete(src);
blobCache.set(src, blobUrl);
while (blobCache.size > MAX_ENTRIES) {
const oldest = blobCache.keys().next().value as string | undefined;
if (!oldest) break;
const oldUrl = blobCache.get(oldest);
blobCache.delete(oldest);
if (oldUrl) URL.revokeObjectURL(oldUrl);
}
};
const fetchBlobUrl = async (src: string): Promise<string> => {
const accessToken = getCurrentAccessToken();
let response = await fetch(src, {
method: 'GET',
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
});
if (!response.ok && response.status === 401 && accessToken) {
response = await fetch(src, { method: 'GET' });
}
if (!response.ok) {
throw new Error(`Failed to fetch authenticated media: ${response.status}`);
}
const blob = await response.blob();
return URL.createObjectURL(blob);
};
/**
* Returns a stable blob: URL for authenticated media, reusing cache across mounts.
* Non-auth URLs are returned as-is.
*/
export const resolveAuthenticatedMediaUrl = async (
src: string,
useAuthentication: boolean
): Promise<string> => {
if (!useAuthentication || !isAuthenticatedMediaUrl(src)) {
return src;
}
const cached = blobCache.get(src);
if (cached) {
touch(src, cached);
return cached;
}
let pending = inflight.get(src);
if (!pending) {
pending = fetchBlobUrl(src)
.then((blobUrl) => {
touch(src, blobUrl);
inflight.delete(src);
return blobUrl;
})
.catch((err) => {
inflight.delete(src);
throw err;
});
inflight.set(src, pending);
}
return pending;
};