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:
168
src/app/state/mediaDimensionCache.ts
Normal file
168
src/app/state/mediaDimensionCache.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { encodeBlurHash, validBlurHash } from '../utils/blurHash';
|
||||
import { fitMediaSize } from '../utils/common';
|
||||
|
||||
const STORAGE_KEY = 'paarrot.media.meta';
|
||||
const LEGACY_DIMENSIONS_KEY = 'paarrot.media.dimensions';
|
||||
const MAX_ENTRIES = 500;
|
||||
|
||||
/** Same max box as MImage / MVideo attachment rendering. */
|
||||
export const ATTACHMENT_MAX_SIZE = 400;
|
||||
|
||||
export type MediaMeta = {
|
||||
w: number;
|
||||
h: number;
|
||||
blurHash?: string;
|
||||
};
|
||||
|
||||
export type MediaDimensions = Pick<MediaMeta, 'w' | 'h'>;
|
||||
|
||||
type CacheMap = Record<string, MediaMeta>;
|
||||
|
||||
let memoryCache: CacheMap | null = null;
|
||||
|
||||
const loadCache = (): CacheMap => {
|
||||
if (memoryCache) return memoryCache;
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) {
|
||||
memoryCache = JSON.parse(raw) as CacheMap;
|
||||
return memoryCache;
|
||||
}
|
||||
// Migrate older dimensions-only cache once.
|
||||
const legacy = localStorage.getItem(LEGACY_DIMENSIONS_KEY);
|
||||
if (legacy) {
|
||||
memoryCache = JSON.parse(legacy) as CacheMap;
|
||||
localStorage.setItem(STORAGE_KEY, legacy);
|
||||
localStorage.removeItem(LEGACY_DIMENSIONS_KEY);
|
||||
return memoryCache;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
memoryCache = {};
|
||||
return memoryCache;
|
||||
};
|
||||
|
||||
const persistCache = (cache: CacheMap) => {
|
||||
memoryCache = cache;
|
||||
try {
|
||||
const keys = Object.keys(cache);
|
||||
if (keys.length > MAX_ENTRIES) {
|
||||
const trimmed: CacheMap = {};
|
||||
keys.slice(keys.length - MAX_ENTRIES).forEach((key) => {
|
||||
trimmed[key] = cache[key];
|
||||
});
|
||||
memoryCache = trimmed;
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(trimmed));
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(cache));
|
||||
} catch {
|
||||
// Quota / private mode — keep in-memory only.
|
||||
}
|
||||
};
|
||||
|
||||
const touchEntry = (mxcUrl: string, patch: Partial<MediaMeta>): void => {
|
||||
if (!mxcUrl) return;
|
||||
const cache = loadCache();
|
||||
const prev = cache[mxcUrl];
|
||||
const next: MediaMeta = {
|
||||
w: patch.w ?? prev?.w ?? 0,
|
||||
h: patch.h ?? prev?.h ?? 0,
|
||||
blurHash: patch.blurHash ?? prev?.blurHash,
|
||||
};
|
||||
if (next.w <= 0 || next.h <= 0) return;
|
||||
if (
|
||||
prev &&
|
||||
prev.w === next.w &&
|
||||
prev.h === next.h &&
|
||||
prev.blurHash === next.blurHash
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (prev) delete cache[mxcUrl];
|
||||
cache[mxcUrl] = next;
|
||||
persistCache(cache);
|
||||
};
|
||||
|
||||
export const getMediaDimensions = (mxcUrl: string): MediaDimensions | undefined => {
|
||||
if (!mxcUrl) return undefined;
|
||||
const entry = loadCache()[mxcUrl];
|
||||
if (!entry || entry.w <= 0 || entry.h <= 0) return undefined;
|
||||
return { w: entry.w, h: entry.h };
|
||||
};
|
||||
|
||||
/** Fit attachment box size using event info, then local dimension cache. */
|
||||
export const resolveAttachmentBoxSize = (
|
||||
mxcUrl: string | undefined,
|
||||
w?: number,
|
||||
h?: number,
|
||||
maxSize: number = ATTACHMENT_MAX_SIZE
|
||||
): { width: number; height: number } => {
|
||||
const cached = mxcUrl && (!w || !h) ? getMediaDimensions(mxcUrl) : undefined;
|
||||
const naturalW = w && w > 0 ? w : cached?.w;
|
||||
const naturalH = h && h > 0 ? h : cached?.h;
|
||||
return fitMediaSize(naturalW ?? 0, naturalH ?? 0, maxSize, maxSize);
|
||||
};
|
||||
|
||||
export const setMediaDimensions = (mxcUrl: string, w: number, h: number): void => {
|
||||
if (!mxcUrl || w <= 0 || h <= 0) return;
|
||||
touchEntry(mxcUrl, { w, h });
|
||||
};
|
||||
|
||||
export const getMediaBlurHash = (mxcUrl: string): string | undefined => {
|
||||
if (!mxcUrl) return undefined;
|
||||
return validBlurHash(loadCache()[mxcUrl]?.blurHash);
|
||||
};
|
||||
|
||||
export const setMediaBlurHash = (mxcUrl: string, blurHash: string, w?: number, h?: number): void => {
|
||||
const hash = validBlurHash(blurHash);
|
||||
if (!mxcUrl || !hash) return;
|
||||
const cache = loadCache();
|
||||
const prev = cache[mxcUrl];
|
||||
const nextW = w && w > 0 ? w : prev?.w;
|
||||
const nextH = h && h > 0 ? h : prev?.h;
|
||||
if (!nextW || !nextH) {
|
||||
// Dimensions required for the shared meta entry; skip until we know size.
|
||||
return;
|
||||
}
|
||||
touchEntry(mxcUrl, { w: nextW, h: nextH, blurHash: hash });
|
||||
};
|
||||
|
||||
/** Encode + cache a blurhash from a loaded image/video (idle, non-blocking). */
|
||||
export const rememberMediaBlurHash = (
|
||||
mxcUrl: string,
|
||||
media: HTMLImageElement | HTMLVideoElement
|
||||
): void => {
|
||||
if (!mxcUrl || typeof window === 'undefined') return;
|
||||
|
||||
const run = () => {
|
||||
try {
|
||||
const w =
|
||||
media instanceof HTMLVideoElement
|
||||
? media.videoWidth
|
||||
: media.naturalWidth || media.width;
|
||||
const h =
|
||||
media instanceof HTMLVideoElement
|
||||
? media.videoHeight
|
||||
: media.naturalHeight || media.height;
|
||||
if (w <= 0 || h <= 0) return;
|
||||
|
||||
setMediaDimensions(mxcUrl, w, h);
|
||||
if (getMediaBlurHash(mxcUrl)) return;
|
||||
|
||||
const encW = 32;
|
||||
const encH = Math.max(1, Math.round(encW * (h / w)));
|
||||
const hash = encodeBlurHash(media, encW, encH);
|
||||
if (hash) setMediaBlurHash(mxcUrl, hash, w, h);
|
||||
} catch {
|
||||
// Encoding can fail for tainted canvases; ignore.
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof window.requestIdleCallback === 'function') {
|
||||
window.requestIdleCallback(() => run(), { timeout: 1500 });
|
||||
} else {
|
||||
window.setTimeout(run, 0);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user