Commit 85334efe0e3a
tarball3 files changed+23-6
@@ -40,7 +40,7 @@
4040
</span>4141
{/if}4242
{#if imageSrc}43
- <img class="link-image" src={imageSrc} alt="" loading="lazy" on:error={() => (imageFailed = true)} />43
+ <img class="link-image" src={imageSrc} alt="" width={image?.width ?? undefined} height={image?.height ?? undefined} loading="lazy" on:error={() => (imageFailed = true)} />4444
{/if}4545
</a>4646
@@ -560,9 +560,9 @@
560560
<MediaAttachmentView attachment={m.media} />561561
{/if}562562
{#if cards.length > 0}563
- <div class="link-previews" transition:expand>563
+ <div class="link-previews" in:dissolve>564564
{#each cards as card (card.link.href)}565
- <div transition:expand><LinkCard link={card.link} preview={card.preview} /></div>565
+ <div in:dissolve><LinkCard link={card.link} preview={card.preview} /></div>566566
{/each}567567
</div>568568
{/if}@@ -5,6 +5,7 @@ import type { LinkPreview } from './types';
55
66
const EVENT_TYPE = 'dev.malice.url_previews';77
const BACKOFF_MS = 30_000;8
+const BATCH_MS = 150;89
910
export interface UrlPreviewSettings {1011
encrypted_rooms: boolean;@@ -16,18 +17,31 @@ export const linkPreviews = writable<Record<string, LinkPreview | null>>({});
1617
export const urlPreviewSettings = writable<UrlPreviewSettings>(DEFAULT_SETTINGS);1718
1819
const inFlight = new Set<string>();20
+const landed: Record<string, LinkPreview | null> = {};21
+let flushTimer: ReturnType<typeof setTimeout> | null = null;22
+23
+/** Previews resolve one by one after a room opens, and each one landing on its own shifted the timeline again. */24
+function land(url: string, preview: LinkPreview | null): void {25
+ landed[url] = preview;26
+ flushTimer ??= setTimeout(() => {27
+ flushTimer = null;28
+ const batch = { ...landed };29
+ for (const key of Object.keys(landed)) delete landed[key];30
+ linkPreviews.update((all) => ({ ...all, ...batch }));31
+ }, BATCH_MS);32
+}1933
let backoffUntil = 0;2034
let refusalLogged = false;2135
2236
export function requestLinkPreview(roomId: string, url: string): void {23
- if (inFlight.has(url) || url in get(linkPreviews) || Date.now() < backoffUntil) return;37
+ if (inFlight.has(url) || url in landed || url in get(linkPreviews) || Date.now() < backoffUntil) return;2438
inFlight.add(url);2539
getUrlPreview(roomId, url)26
- .then((raw) => linkPreviews.update((all) => ({ ...all, [url]: parseLinkPreview(raw) })))40
+ .then((raw) => land(url, parseLinkPreview(raw)))2741
.catch((e) => {2842
if (e instanceof ApiError && e.status === 429) backoffUntil = Date.now() + BACKOFF_MS;2943
else if (e instanceof ApiError && e.status === 403) {30
- linkPreviews.update((all) => ({ ...all, [url]: null }));44
+ land(url, null);3145
if (!refusalLogged) console.warn('The homeserver refused a link preview', e.message);3246
refusalLogged = true;3347
}@@ -57,5 +71,8 @@ export function resetLinkPreviews(): void {
5771
linkPreviews.set({});5872
urlPreviewSettings.set(DEFAULT_SETTINGS);5973
inFlight.clear();74
+ if (flushTimer) clearTimeout(flushTimer);75
+ flushTimer = null;76
+ for (const key of Object.keys(landed)) delete landed[key];6077
backoffUntil = 0;6178
}