fix(chat): make cached media load on safari

This commit is contained in:
2026-07-01 20:13:56 +08:00
parent 8586e87ab6
commit c20769b6a3
11 changed files with 299 additions and 72 deletions
@@ -109,10 +109,13 @@ export class ChatMediaCacheCoordinator {
);
}
const blob = await response.blob();
if (blob.size <= 0) {
throw new Error("Media download returned an empty blob.");
const bytes = await response.arrayBuffer();
if (bytes.byteLength <= 0) {
throw new Error("Media download returned empty bytes.");
}
const mimeType =
response.headers.get("content-type") ||
fallbackChatMediaMimeType(input.kind);
const saveResult = await this.mediaStorage.saveMedia({
cacheKey,
@@ -120,11 +123,8 @@ export class ChatMediaCacheCoordinator {
messageId: input.messageId,
kind: input.kind,
remoteUrl: input.remoteUrl,
blob,
mimeType:
blob.type ||
response.headers.get("content-type") ||
fallbackChatMediaMimeType(input.kind),
bytes,
mimeType,
});
if (Result.isErr(saveResult)) throw saveResult.error;
void requestBrowserPersistentStorageOnce();
@@ -69,12 +69,12 @@ export interface IChatRepository {
/** 获取本地消息数量。 */
getLocalMessageCount(): Promise<Result<number>>;
/** 获取本地缓存的图片 / 音频 Blob。 */
/** 获取本地缓存的图片 / 音频。 */
getCachedMedia(
input: ChatMediaLookupInput,
): Promise<Result<LocalChatMediaRow | null>>;
/** 下载并缓存远程图片 / 音频 Blob。 */
/** 下载并缓存远程图片 / 音频。 */
cacheRemoteMedia(
input: CacheRemoteChatMediaInput,
): Promise<Result<LocalChatMediaRow>>;
+4 -1
View File
@@ -39,7 +39,10 @@ export interface LocalChatMediaRow {
messageId: string;
kind: ChatMediaKind;
remoteUrl: string;
blob: Blob;
/** v3+ stores raw bytes instead of Blob to avoid iOS WebKit Blob URL issues. */
bytes?: ArrayBuffer;
/** Legacy v2 field. Read-only fallback for users who already have cached media. */
blob?: Blob;
mimeType: string;
byteSize: number;
createdAt: number;
@@ -16,7 +16,7 @@ export interface SaveLocalChatMediaInput {
messageId: string;
kind: ChatMediaKind;
remoteUrl: string;
blob: Blob;
bytes: ArrayBuffer;
mimeType: string;
}
@@ -44,9 +44,9 @@ export class LocalChatMediaStorage {
try {
const row = (await this.db.media.get(cacheKey)) ?? null;
if (row) {
await this.db.media.update(cacheKey, {
lastAccessedAt: Date.now(),
});
const normalizedRow = await this.normalizeLegacyMediaRow(row);
await this.db.media.update(cacheKey, { lastAccessedAt: Date.now() });
return Result.ok(normalizedRow);
}
return Result.ok(row);
} catch (e) {
@@ -64,9 +64,9 @@ export class LocalChatMediaStorage {
messageId: input.messageId,
kind: input.kind,
remoteUrl: input.remoteUrl,
blob: input.blob,
bytes: input.bytes,
mimeType: input.mimeType,
byteSize: input.blob.size,
byteSize: input.bytes.byteLength,
createdAt: existing?.createdAt ?? now,
updatedAt: now,
lastAccessedAt: now,
@@ -85,4 +85,35 @@ export class LocalChatMediaStorage {
return Result.err(e);
}
}
private async normalizeLegacyMediaRow(
row: LocalChatMediaRow,
): Promise<LocalChatMediaRow> {
if (row.bytes && row.bytes.byteLength > 0) return row;
if (!row.blob || row.blob.size <= 0) return row;
const bytes = await row.blob.arrayBuffer();
const now = Date.now();
const normalizedRow: LocalChatMediaRow = {
cacheKey: row.cacheKey,
ownerKey: row.ownerKey,
messageId: row.messageId,
kind: row.kind,
remoteUrl: row.remoteUrl,
bytes,
mimeType:
row.mimeType || row.blob.type || fallbackMediaMimeType(row.kind),
byteSize: bytes.byteLength,
createdAt: row.createdAt,
updatedAt: now,
lastAccessedAt: now,
};
await this.db.media.put(normalizedRow);
return normalizedRow;
}
}
function fallbackMediaMimeType(kind: ChatMediaKind): string {
return kind === "image" ? "image/*" : "audio/mpeg";
}