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.
88 lines
2.4 KiB
TypeScript
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;
|
|
};
|