diff --git a/src/app/chat/components/fullscreen-image-viewer.tsx b/src/app/chat/components/fullscreen-image-viewer.tsx index e0b861de..d1b4d853 100644 --- a/src/app/chat/components/fullscreen-image-viewer.tsx +++ b/src/app/chat/components/fullscreen-image-viewer.tsx @@ -14,7 +14,12 @@ import { ChevronLeft } from "lucide-react"; import Image from "next/image"; import { useEffect } from "react"; -import { useCachedChatMediaUrl } from "../hooks/use-cached-chat-media-url"; +import { + isBrowserLocalChatImageSource, + normalizeChatImageSource, +} from "@/lib/chat/chat_media_url"; +import { useCachedChatMediaUrl } from "@/lib/chat/use_cached_chat_media_url"; + import styles from "./fullscreen-image-viewer.module.css"; export interface FullscreenImageViewerProps { @@ -46,13 +51,8 @@ export function FullscreenImageViewer({ return () => document.removeEventListener("keydown", handleKey); }, [onClose]); - const sourceUrl = mediaUrl || imageUrl; - const src = sourceUrl.startsWith("data:") || - sourceUrl.startsWith("http") || - sourceUrl.startsWith("blob:") - ? sourceUrl - : `data:image/png;base64,${sourceUrl}`; - const shouldUseNativeImage = isBrowserLocalImageSrc(src); + const src = normalizeChatImageSource(mediaUrl || imageUrl); + const shouldUseNativeImage = isBrowserLocalChatImageSource(src); if (imagePaywalled) { return ( @@ -131,7 +131,3 @@ export function FullscreenImageViewer({ ); } - -function isBrowserLocalImageSrc(src: string): boolean { - return src.startsWith("blob:") || src.startsWith("data:"); -} diff --git a/src/app/chat/components/image-bubble.tsx b/src/app/chat/components/image-bubble.tsx index b650f8d7..d447830f 100644 --- a/src/app/chat/components/image-bubble.tsx +++ b/src/app/chat/components/image-bubble.tsx @@ -13,8 +13,12 @@ import { useRouter } from "next/navigation"; import { useState } from "react"; import { ROUTE_BUILDERS } from "@/router/routes"; +import { + isBrowserLocalChatImageSource, + normalizeChatImageSource, +} from "@/lib/chat/chat_media_url"; +import { useCachedChatMediaUrl } from "@/lib/chat/use_cached_chat_media_url"; -import { useCachedChatMediaUrl } from "../hooks/use-cached-chat-media-url"; import styles from "./image-bubble.module.css"; export interface ImageBubbleProps { @@ -36,9 +40,9 @@ export function ImageBubble({ kind: "image", }); - const src = decodeBase64Image(mediaUrl || imageUrl); + const src = normalizeChatImageSource(mediaUrl || imageUrl); const canOpen = Boolean(messageId); - const shouldUseNativeImage = isBrowserLocalImageSrc(src); + const shouldUseNativeImage = isBrowserLocalChatImageSource(src); const openImage = () => { if (!messageId) return; @@ -85,17 +89,3 @@ export function ImageBubble({ ); } - -/** 解析 base64 data URI(`data:image/png;base64,...`),否则原样返回 */ -function decodeBase64Image(uri: string): string { - // 已是 data URI 或 http(s) URL:直接返回 - if (uri.startsWith("data:") || uri.startsWith("http")) { - return uri; - } - // 裸 base64 字符串 → 包装成 data URI(PNG 兜底) - return `data:image/png;base64,${uri}`; -} - -function isBrowserLocalImageSrc(src: string): boolean { - return src.startsWith("blob:") || src.startsWith("data:"); -} diff --git a/src/app/chat/components/voice-bubble.tsx b/src/app/chat/components/voice-bubble.tsx index de416a63..8e9cfb5b 100644 --- a/src/app/chat/components/voice-bubble.tsx +++ b/src/app/chat/components/voice-bubble.tsx @@ -3,7 +3,7 @@ import { LockKeyhole, Pause, Play } from "lucide-react"; import { useEffect, useRef, useState } from "react"; -import { useCachedChatMediaUrl } from "../hooks/use-cached-chat-media-url"; +import { useCachedChatMediaUrl } from "@/lib/chat/use_cached_chat_media_url"; import styles from "./voice-bubble.module.css"; export interface VoiceBubbleProps { diff --git a/src/data/dto/chat/chat_media.ts b/src/data/dto/chat/chat_media.ts new file mode 100644 index 00000000..18e2d9a3 --- /dev/null +++ b/src/data/dto/chat/chat_media.ts @@ -0,0 +1 @@ +export type ChatMediaKind = "image" | "audio"; diff --git a/src/data/dto/chat/index.ts b/src/data/dto/chat/index.ts index 4a7e0f06..ba840084 100644 --- a/src/data/dto/chat/index.ts +++ b/src/data/dto/chat/index.ts @@ -4,6 +4,7 @@ export * from "./chat_history_response"; export * from "./chat_list_item"; +export * from "./chat_media"; export * from "./chat_message"; export * from "./chat_send_response"; export * from "./chat_sync_data"; diff --git a/src/data/repositories/__tests__/chat_media_cache_helpers.test.ts b/src/data/repositories/__tests__/chat_media_cache_helpers.test.ts new file mode 100644 index 00000000..43983e9f --- /dev/null +++ b/src/data/repositories/__tests__/chat_media_cache_helpers.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, it } from "vitest"; + +import { ChatMessage, ChatSendResponse } from "@/data/dto/chat"; + +import { + buildChatMediaCacheKey, + fallbackChatMediaMimeType, + getMessageMediaTargets, + getSendResponseMediaTargets, + uniqueMediaTargets, +} from "../chat_media_cache_helpers"; + +function makeMessage( + overrides: Partial[0]> = {}, +): ChatMessage { + return ChatMessage.from({ + id: "msg-1", + role: "assistant", + type: "image", + content: "", + createdAt: "2026-06-30T00:00:00.000Z", + audioUrl: null, + image: { type: null, url: null }, + lockDetail: { + locked: false, + showContent: true, + showUpgrade: false, + reason: null, + hint: null, + detail: null, + }, + ...overrides, + }); +} + +function makeResponse( + overrides: Partial[0]> = {}, +): ChatSendResponse { + return ChatSendResponse.from({ + reply: "", + audioUrl: "", + messageId: "msg-1", + isGuest: false, + timestamp: 1782356425363, + image: { type: null, url: null }, + lockDetail: { + locked: false, + showContent: true, + showUpgrade: false, + reason: null, + hint: null, + detail: null, + }, + ...overrides, + }); +} + +describe("chat media cache helpers", () => { + it("builds owner-scoped media cache keys", () => { + expect( + buildChatMediaCacheKey({ + ownerKey: "user:u1", + messageId: "msg-1", + kind: "image", + }), + ).toBe("user:u1:msg-1:image"); + }); + + it("extracts cacheable media targets from history messages", () => { + expect( + getMessageMediaTargets( + makeMessage({ + audioUrl: "https://example.com/a.mp3", + image: { type: "jpg", url: "https://example.com/a.jpg" }, + }), + ), + ).toEqual([ + { + messageId: "msg-1", + kind: "image", + remoteUrl: "https://example.com/a.jpg", + }, + { + messageId: "msg-1", + kind: "audio", + remoteUrl: "https://example.com/a.mp3", + }, + ]); + }); + + it("ignores empty message ids and non-remote media urls", () => { + expect( + getSendResponseMediaTargets( + makeResponse({ + messageId: "", + audioUrl: "https://example.com/a.mp3", + image: { type: "png", url: "data:image/png;base64,abc" }, + }), + ), + ).toEqual([]); + }); + + it("deduplicates media targets", () => { + expect( + uniqueMediaTargets([ + { messageId: "msg-1", kind: "image", remoteUrl: "https://x/a.jpg" }, + { messageId: "msg-1", kind: "image", remoteUrl: "https://x/a.jpg" }, + { messageId: "msg-1", kind: "audio", remoteUrl: "https://x/a.jpg" }, + ]), + ).toEqual([ + { messageId: "msg-1", kind: "image", remoteUrl: "https://x/a.jpg" }, + { messageId: "msg-1", kind: "audio", remoteUrl: "https://x/a.jpg" }, + ]); + }); + + it("provides fallback mime types", () => { + expect(fallbackChatMediaMimeType("image")).toBe("image/*"); + expect(fallbackChatMediaMimeType("audio")).toBe("audio/mpeg"); + }); +}); diff --git a/src/data/repositories/chat_media_cache_helpers.ts b/src/data/repositories/chat_media_cache_helpers.ts new file mode 100644 index 00000000..de7e1a23 --- /dev/null +++ b/src/data/repositories/chat_media_cache_helpers.ts @@ -0,0 +1,89 @@ +import type { + ChatMediaKind, + ChatMessage, + ChatSendResponse, +} from "@/data/dto/chat"; +import type { CacheRemoteChatMediaInput } from "@/data/repositories/interfaces"; +import { isCacheableRemoteChatMediaUrl } from "@/lib/chat/chat_media_url"; + +export { isCacheableRemoteChatMediaUrl }; + +export function buildChatMediaCacheKey(input: { + ownerKey: string; + messageId: string; + kind: ChatMediaKind; +}): string { + return `${input.ownerKey}:${input.messageId}:${input.kind}`; +} + +export function getMessageMediaTargets( + message: ChatMessage, +): CacheRemoteChatMediaInput[] { + const targets: CacheRemoteChatMediaInput[] = []; + pushMediaTarget(targets, { + messageId: message.id, + kind: "image", + remoteUrl: message.image.url, + }); + pushMediaTarget(targets, { + messageId: message.id, + kind: "audio", + remoteUrl: message.audioUrl, + }); + return targets; +} + +export function getSendResponseMediaTargets( + response: ChatSendResponse, +): CacheRemoteChatMediaInput[] { + const targets: CacheRemoteChatMediaInput[] = []; + pushMediaTarget(targets, { + messageId: response.messageId, + kind: "image", + remoteUrl: response.image.url, + }); + pushMediaTarget(targets, { + messageId: response.messageId, + kind: "audio", + remoteUrl: response.audioUrl, + }); + return targets; +} + +export function fallbackChatMediaMimeType(kind: ChatMediaKind): string { + return kind === "image" ? "image/*" : "audio/mpeg"; +} + +export function uniqueMediaTargets( + targets: readonly CacheRemoteChatMediaInput[], +): CacheRemoteChatMediaInput[] { + const seen = new Set(); + return targets.filter((target) => { + const key = `${target.messageId}:${target.kind}:${target.remoteUrl}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); +} + +function pushMediaTarget( + targets: CacheRemoteChatMediaInput[], + input: { + messageId: string; + kind: ChatMediaKind; + remoteUrl: string | null; + }, +): void { + if (input.messageId.length === 0) return; + if ( + !input.remoteUrl || + !isCacheableRemoteChatMediaUrl(input.remoteUrl) + ) { + return; + } + targets.push({ + messageId: input.messageId, + kind: input.kind, + remoteUrl: input.remoteUrl, + }); +} diff --git a/src/data/repositories/chat_repository.ts b/src/data/repositories/chat_repository.ts index 49c23b7c..27a9b2af 100644 --- a/src/data/repositories/chat_repository.ts +++ b/src/data/repositories/chat_repository.ts @@ -22,10 +22,18 @@ import { LocalChatMediaStorage, LocalChatStorage, LocalMessage, - type LocalChatMediaKind, type LocalChatMediaRow, } from "@/data/storage/chat"; +import { + buildChatMediaCacheKey, + fallbackChatMediaMimeType, + getMessageMediaTargets, + getSendResponseMediaTargets, + isCacheableRemoteChatMediaUrl, + uniqueMediaTargets, +} from "./chat_media_cache_helpers"; import { createLazySingleton } from "./lazy_singleton"; +import { requestBrowserPersistentStorageOnce } from "@/lib/browser/persistent_storage"; const log = new Logger("DataRepositoriesChatRepository"); @@ -180,7 +188,7 @@ export class ChatRepository implements IChatRepository { input: CacheRemoteChatMediaInput, ): Promise> { return Result.wrap(async () => { - if (!isCacheableRemoteUrl(input.remoteUrl)) { + if (!isCacheableRemoteChatMediaUrl(input.remoteUrl)) { throw new Error(`Unsupported media url: ${input.remoteUrl}`); } @@ -217,10 +225,12 @@ export class ChatRepository implements IChatRepository { remoteUrl: input.remoteUrl, blob, mimeType: - blob.type || response.headers.get("content-type") || fallbackMimeType(input.kind), + blob.type || + response.headers.get("content-type") || + fallbackChatMediaMimeType(input.kind), }); if (Result.isErr(saveResult)) throw saveResult.error; - void requestPersistentStorage(); + void requestBrowserPersistentStorageOnce(); const savedResult = await this.mediaStorage.getMedia(cacheKey); if (Result.isErr(savedResult)) throw savedResult.error; @@ -333,7 +343,11 @@ export class ChatRepository implements IChatRepository { ownerKey: string, input: ChatMediaLookupInput, ): string { - return `${ownerKey}:${input.messageId}:${input.kind}`; + return buildChatMediaCacheKey({ + ownerKey, + messageId: input.messageId, + kind: input.kind, + }); } } @@ -346,90 +360,3 @@ export const getChatRepository = createLazySingleton( LocalChatMediaStorage.getInstance(), ), ); - -function getMessageMediaTargets( - message: ChatMessage, -): CacheRemoteChatMediaInput[] { - const targets: CacheRemoteChatMediaInput[] = []; - pushMediaTarget(targets, { - messageId: message.id, - kind: "image", - remoteUrl: message.image.url, - }); - pushMediaTarget(targets, { - messageId: message.id, - kind: "audio", - remoteUrl: message.audioUrl, - }); - return targets; -} - -function getSendResponseMediaTargets( - response: ChatSendResponse, -): CacheRemoteChatMediaInput[] { - const targets: CacheRemoteChatMediaInput[] = []; - pushMediaTarget(targets, { - messageId: response.messageId, - kind: "image", - remoteUrl: response.image.url, - }); - pushMediaTarget(targets, { - messageId: response.messageId, - kind: "audio", - remoteUrl: response.audioUrl, - }); - return targets; -} - -function pushMediaTarget( - targets: CacheRemoteChatMediaInput[], - input: { - messageId: string; - kind: LocalChatMediaKind; - remoteUrl: string | null; - }, -): void { - if (input.messageId.length === 0) return; - if (!input.remoteUrl || !isCacheableRemoteUrl(input.remoteUrl)) return; - targets.push({ - messageId: input.messageId, - kind: input.kind, - remoteUrl: input.remoteUrl, - }); -} - -function isCacheableRemoteUrl(url: string): boolean { - return url.startsWith("http://") || url.startsWith("https://"); -} - -function fallbackMimeType(kind: LocalChatMediaKind): string { - return kind === "image" ? "image/*" : "audio/mpeg"; -} - -function uniqueMediaTargets( - targets: readonly CacheRemoteChatMediaInput[], -): CacheRemoteChatMediaInput[] { - const seen = new Set(); - return targets.filter((target) => { - const key = `${target.messageId}:${target.kind}:${target.remoteUrl}`; - if (seen.has(key)) return false; - seen.add(key); - return true; - }); -} - -let storagePersistenceRequested = false; - -async function requestPersistentStorage(): Promise { - if (storagePersistenceRequested) return; - storagePersistenceRequested = true; - if (typeof navigator === "undefined") return; - if (!navigator.storage?.persist) return; - - try { - const persisted = await navigator.storage.persist(); - log.debug("[chat-media] persistent storage requested", { persisted }); - } catch (error) { - log.debug("[chat-media] persistent storage request skipped", { error }); - } -} diff --git a/src/data/repositories/interfaces/ichat_repository.ts b/src/data/repositories/interfaces/ichat_repository.ts index d1a7ce07..568679a4 100644 --- a/src/data/repositories/interfaces/ichat_repository.ts +++ b/src/data/repositories/interfaces/ichat_repository.ts @@ -14,19 +14,17 @@ import type { Result } from "@/utils"; import type { ChatHistoryResponse, + ChatMediaKind, ChatMessage, ChatSendResponse, UnlockHistoryResponse, UnlockPrivateResponse, } from "@/data/dto/chat"; -import type { - LocalChatMediaKind, - LocalChatMediaRow, -} from "@/data/storage/chat"; +import type { LocalChatMediaRow } from "@/data/storage/chat"; export interface ChatMediaLookupInput { messageId: string; - kind: LocalChatMediaKind; + kind: ChatMediaKind; } export interface CacheRemoteChatMediaInput extends ChatMediaLookupInput { diff --git a/src/data/storage/chat/local_chat_db.ts b/src/data/storage/chat/local_chat_db.ts index e4e0c62c..99fd85ca 100644 --- a/src/data/storage/chat/local_chat_db.ts +++ b/src/data/storage/chat/local_chat_db.ts @@ -12,6 +12,7 @@ */ import Dexie, { type Table } from "dexie"; +import type { ChatMediaKind } from "@/data/dto/chat"; import type { ChatImageData, ChatLockDetailData, @@ -32,13 +33,11 @@ export interface LocalMessageRow { sessionId: string; } -export type LocalChatMediaKind = "image" | "audio"; - export interface LocalChatMediaRow { cacheKey: string; ownerKey: string; messageId: string; - kind: LocalChatMediaKind; + kind: ChatMediaKind; remoteUrl: string; blob: Blob; mimeType: string; diff --git a/src/data/storage/chat/local_chat_media_storage.ts b/src/data/storage/chat/local_chat_media_storage.ts index f0c7f63f..c40f4fc4 100644 --- a/src/data/storage/chat/local_chat_media_storage.ts +++ b/src/data/storage/chat/local_chat_media_storage.ts @@ -1,20 +1,20 @@ "use client"; import { Result, type Result as ResultT } from "@/utils"; +import type { ChatMediaKind } from "@/data/dto/chat"; import { LocalChatDB, - type LocalChatMediaKind, type LocalChatMediaRow, } from "./local_chat_db"; -export type { LocalChatMediaKind, LocalChatMediaRow }; +export type { LocalChatMediaRow }; export interface SaveLocalChatMediaInput { cacheKey: string; ownerKey: string; messageId: string; - kind: LocalChatMediaKind; + kind: ChatMediaKind; remoteUrl: string; blob: Blob; mimeType: string; diff --git a/src/lib/browser/persistent_storage.ts b/src/lib/browser/persistent_storage.ts new file mode 100644 index 00000000..3cb9980a --- /dev/null +++ b/src/lib/browser/persistent_storage.ts @@ -0,0 +1,20 @@ +"use client"; + +import { Logger } from "@/utils"; + +const log = new Logger("LibBrowserPersistentStorage"); +let storagePersistenceRequested = false; + +export async function requestBrowserPersistentStorageOnce(): Promise { + if (storagePersistenceRequested) return; + storagePersistenceRequested = true; + if (typeof navigator === "undefined") return; + if (!navigator.storage?.persist) return; + + try { + const persisted = await navigator.storage.persist(); + log.debug("[persistent-storage] requested", { persisted }); + } catch (error) { + log.debug("[persistent-storage] request skipped", { error }); + } +} diff --git a/src/lib/chat/__tests__/chat_media_url.test.ts b/src/lib/chat/__tests__/chat_media_url.test.ts new file mode 100644 index 00000000..457149ec --- /dev/null +++ b/src/lib/chat/__tests__/chat_media_url.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; + +import { + isBrowserLocalChatImageSource, + isCacheableRemoteChatMediaUrl, + normalizeChatImageSource, +} from "../chat_media_url"; + +describe("chat media url helpers", () => { + it("detects cacheable remote media urls", () => { + expect(isCacheableRemoteChatMediaUrl("https://example.com/a.jpg")).toBe( + true, + ); + expect(isCacheableRemoteChatMediaUrl("http://example.com/a.jpg")).toBe( + true, + ); + expect(isCacheableRemoteChatMediaUrl("blob:http://app/media")).toBe(false); + expect(isCacheableRemoteChatMediaUrl("data:image/png;base64,abc")).toBe( + false, + ); + }); + + it("normalizes bare base64 image payloads", () => { + expect(normalizeChatImageSource("abc123")).toBe( + "data:image/png;base64,abc123", + ); + expect(normalizeChatImageSource("blob:http://app/media")).toBe( + "blob:http://app/media", + ); + expect(normalizeChatImageSource("https://example.com/a.jpg")).toBe( + "https://example.com/a.jpg", + ); + }); + + it("detects image sources that should bypass Next image optimization", () => { + expect(isBrowserLocalChatImageSource("blob:http://app/media")).toBe(true); + expect(isBrowserLocalChatImageSource("data:image/png;base64,abc")).toBe( + true, + ); + expect(isBrowserLocalChatImageSource("https://example.com/a.jpg")).toBe( + false, + ); + }); +}); diff --git a/src/lib/chat/chat_media_cache_client.ts b/src/lib/chat/chat_media_cache_client.ts new file mode 100644 index 00000000..341a8219 --- /dev/null +++ b/src/lib/chat/chat_media_cache_client.ts @@ -0,0 +1,31 @@ +"use client"; + +import type { ChatMediaKind } from "@/data/dto/chat"; +import { getChatRepository } from "@/data/repositories/chat_repository"; +import { Result, type Result as ResultT } from "@/utils"; + +export interface ResolveCachedChatMediaBlobInput { + messageId: string; + kind: ChatMediaKind; +} + +export interface CacheRemoteChatMediaBlobInput + extends ResolveCachedChatMediaBlobInput { + remoteUrl: string; +} + +export async function resolveCachedChatMediaBlob( + input: ResolveCachedChatMediaBlobInput, +): Promise> { + const result = await getChatRepository().getCachedMedia(input); + if (Result.isErr(result)) return result; + return Result.ok(result.data?.blob ?? null); +} + +export async function cacheRemoteChatMediaBlob( + input: CacheRemoteChatMediaBlobInput, +): Promise> { + const result = await getChatRepository().cacheRemoteMedia(input); + if (Result.isErr(result)) return result; + return Result.ok(result.data.blob); +} diff --git a/src/lib/chat/chat_media_url.ts b/src/lib/chat/chat_media_url.ts new file mode 100644 index 00000000..338d3bbe --- /dev/null +++ b/src/lib/chat/chat_media_url.ts @@ -0,0 +1,18 @@ +export function isCacheableRemoteChatMediaUrl(url: string): boolean { + return url.startsWith("http://") || url.startsWith("https://"); +} + +export function normalizeChatImageSource(url: string): string { + if ( + url.startsWith("data:") || + url.startsWith("http") || + url.startsWith("blob:") + ) { + return url; + } + return `data:image/png;base64,${url}`; +} + +export function isBrowserLocalChatImageSource(src: string): boolean { + return src.startsWith("blob:") || src.startsWith("data:"); +} diff --git a/src/app/chat/hooks/use-cached-chat-media-url.ts b/src/lib/chat/use_cached_chat_media_url.ts similarity index 75% rename from src/app/chat/hooks/use-cached-chat-media-url.ts rename to src/lib/chat/use_cached_chat_media_url.ts index f46d88a2..01e97c08 100644 --- a/src/app/chat/hooks/use-cached-chat-media-url.ts +++ b/src/lib/chat/use_cached_chat_media_url.ts @@ -2,14 +2,19 @@ import { useEffect, useState } from "react"; -import type { LocalChatMediaKind } from "@/data/storage/chat"; -import { getChatRepository } from "@/data/repositories/chat_repository"; +import type { ChatMediaKind } from "@/data/dto/chat"; import { Result } from "@/utils"; +import { + cacheRemoteChatMediaBlob, + resolveCachedChatMediaBlob, +} from "./chat_media_cache_client"; +import { isCacheableRemoteChatMediaUrl } from "./chat_media_url"; + export interface UseCachedChatMediaUrlInput { messageId?: string | null; remoteUrl?: string | null; - kind: LocalChatMediaKind; + kind: ChatMediaKind; } export interface UseCachedChatMediaUrlOutput { @@ -35,7 +40,11 @@ export function useCachedChatMediaUrl({ messageId && remoteUrl ? `${messageId}:${kind}:${remoteUrl}` : ""; useEffect(() => { - if (!messageId || !remoteUrl || !isCacheableRemoteUrl(remoteUrl)) { + if ( + !messageId || + !remoteUrl || + !isCacheableRemoteChatMediaUrl(remoteUrl) + ) { return; } @@ -59,16 +68,18 @@ export function useCachedChatMediaUrl({ }; const resolveCachedMedia = async () => { - const chatRepo = getChatRepository(); - const cachedResult = await chatRepo.getCachedMedia({ messageId, kind }); + const cachedResult = await resolveCachedChatMediaBlob({ + messageId, + kind, + }); if (cancelled) return; if (Result.isOk(cachedResult) && cachedResult.data) { - applyBlob(cachedResult.data.blob); + applyBlob(cachedResult.data); return; } - const cacheResult = await chatRepo.cacheRemoteMedia({ + const cacheResult = await cacheRemoteChatMediaBlob({ messageId, kind, remoteUrl, @@ -76,7 +87,7 @@ export function useCachedChatMediaUrl({ if (cancelled) return; if (Result.isOk(cacheResult)) { - applyBlob(cacheResult.data.blob); + applyBlob(cacheResult.data); } }; @@ -100,7 +111,3 @@ export function useCachedChatMediaUrl({ isUsingCachedMedia: false, }; } - -function isCacheableRemoteUrl(url: string): boolean { - return url.startsWith("http://") || url.startsWith("https://"); -} diff --git a/src/stores/chat/chat-machine.actors.ts b/src/stores/chat/chat-machine.actors.ts index 2dc435ef..1a72e90d 100644 --- a/src/stores/chat/chat-machine.actors.ts +++ b/src/stores/chat/chat-machine.actors.ts @@ -67,6 +67,7 @@ export const loadMoreHistoryActor = fromPromise< throw result.error; } const page = localMessagesToUi(result.data.messages); + void chatRepo.prefetchMediaForMessages(result.data.messages); return { messages: page, hasMore: page.length >= PAGE_SIZE,