feat(media): implement caching for chat media and update components to use cached media

This commit is contained in:
2026-06-30 15:59:33 +08:00
parent 78d8aae22e
commit ccd2d6bd48
15 changed files with 591 additions and 42 deletions
@@ -0,0 +1,106 @@
"use client";
import { useEffect, useState } from "react";
import type { LocalChatMediaKind } from "@/data/storage/chat";
import { getChatRepository } from "@/data/repositories/chat_repository";
import { Result } from "@/utils";
export interface UseCachedChatMediaUrlInput {
messageId?: string | null;
remoteUrl?: string | null;
kind: LocalChatMediaKind;
}
export interface UseCachedChatMediaUrlOutput {
mediaUrl: string;
isUsingCachedMedia: boolean;
}
interface CachedMediaUrlState {
key: string;
objectUrl: string;
}
export function useCachedChatMediaUrl({
messageId,
remoteUrl,
kind,
}: UseCachedChatMediaUrlInput): UseCachedChatMediaUrlOutput {
const [cachedState, setCachedState] = useState<CachedMediaUrlState | null>(
null,
);
const fallbackUrl = remoteUrl ?? "";
const stateKey =
messageId && remoteUrl ? `${messageId}:${kind}:${remoteUrl}` : "";
useEffect(() => {
if (!messageId || !remoteUrl || !isCacheableRemoteUrl(remoteUrl)) {
return;
}
let cancelled = false;
let objectUrl: string | null = null;
const applyBlob = (blob: Blob) => {
const nextObjectUrl = URL.createObjectURL(blob);
objectUrl = nextObjectUrl;
if (cancelled) {
URL.revokeObjectURL(nextObjectUrl);
return;
}
setCachedState((previous) => {
if (previous) URL.revokeObjectURL(previous.objectUrl);
return {
key: stateKey,
objectUrl: nextObjectUrl,
};
});
};
const resolveCachedMedia = async () => {
const chatRepo = getChatRepository();
const cachedResult = await chatRepo.getCachedMedia({ messageId, kind });
if (cancelled) return;
if (Result.isOk(cachedResult) && cachedResult.data) {
applyBlob(cachedResult.data.blob);
return;
}
const cacheResult = await chatRepo.cacheRemoteMedia({
messageId,
kind,
remoteUrl,
});
if (cancelled) return;
if (Result.isOk(cacheResult)) {
applyBlob(cacheResult.data.blob);
}
};
void resolveCachedMedia();
return () => {
cancelled = true;
if (objectUrl) URL.revokeObjectURL(objectUrl);
};
}, [kind, messageId, remoteUrl, stateKey]);
if (cachedState?.key === stateKey) {
return {
mediaUrl: cachedState.objectUrl,
isUsingCachedMedia: true,
};
}
return {
mediaUrl: fallbackUrl,
isUsingCachedMedia: false,
};
}
function isCacheableRemoteUrl(url: string): boolean {
return url.startsWith("http://") || url.startsWith("https://");
}