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.
This commit is contained in:
87
src/app/utils/authenticatedMediaCache.ts
Normal file
87
src/app/utils/authenticatedMediaCache.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
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;
|
||||
};
|
||||
Reference in New Issue
Block a user