fix(splash): reuse latest chat message preview

This commit is contained in:
2026-07-14 11:28:23 +08:00
parent b4904e738b
commit f9c15bd91f
13 changed files with 389 additions and 18 deletions
+6
View File
@@ -35,6 +35,7 @@ import { useChatUnlockNavigationFlow } from "./hooks/use-chat-unlock-navigation-
import { useChatGuestLogin } from "./hooks/use-chat-guest-login";
import { useChatMessageLimitBanner } from "./hooks/use-chat-message-limit-banner";
import { useChatPromotionBootstrap } from "./hooks/use-chat-promotion-bootstrap";
import { useSplashLatestMessageSync } from "./hooks/use-splash-latest-message-sync";
const chatShellStyle = {
"--chat-message-font-size": "clamp(16px, 3.333vw, 18px)",
@@ -47,6 +48,11 @@ export function ChatScreen() {
const state = useChatState();
const authState = useAuthState();
const authDispatch = useAuthDispatch();
useSplashLatestMessageSync({
historyLoaded: state.historyLoaded,
loginStatus: authState.loginStatus,
messages: state.historyMessages,
});
const isPromotionBootstrapReady = useChatPromotionBootstrap();
const visibleMessages = isPromotionBootstrapReady
? state.messages
@@ -0,0 +1,53 @@
"use client";
import { useEffect, useRef } from "react";
import type { LoginStatus } from "@/data/dto/auth";
import type { UiMessage } from "@/data/dto/chat";
import { resolveSplashLatestMessageCacheIdentity } from "@/lib/chat/splash_latest_message";
import { getLatestSplashMessagePreview } from "@/lib/chat/splash_latest_message_preview";
import { useSplashLatestMessageCache } from "@/providers/splash-latest-message-provider";
export interface UseSplashLatestMessageSyncInput {
historyLoaded: boolean;
loginStatus: LoginStatus;
messages: readonly UiMessage[];
}
export function useSplashLatestMessageSync({
historyLoaded,
loginStatus,
messages,
}: UseSplashLatestMessageSyncInput): void {
const cache = useSplashLatestMessageCache();
const hasObservedInitialSnapshotRef = useRef(false);
const identityPromiseRef = useRef<Promise<string | null> | null>(null);
useEffect(() => {
hasObservedInitialSnapshotRef.current = false;
identityPromiseRef.current = null;
}, [loginStatus]);
useEffect(() => {
if (!historyLoaded) return;
if (!hasObservedInitialSnapshotRef.current) {
hasObservedInitialSnapshotRef.current = true;
return;
}
const preview = getLatestSplashMessagePreview(messages);
let cancelled = false;
identityPromiseRef.current ??= resolveSplashLatestMessageCacheIdentity();
void identityPromiseRef.current.then((identity) => {
if (!cancelled && identity) {
cache.set(identity, preview);
}
});
return () => {
cancelled = true;
};
}, [cache, historyLoaded, messages]);
}
@@ -1,6 +1,9 @@
import { describe, expect, it } from "vitest";
import { getSplashLatestMessagePreview } from "@/lib/chat/splash_latest_message_preview";
import {
getLatestSplashMessagePreview,
getSplashLatestMessagePreview,
} from "@/lib/chat/splash_latest_message_preview";
describe("getSplashLatestMessagePreview", () => {
it("uses text content for text messages", () => {
@@ -34,6 +37,15 @@ describe("getSplashLatestMessagePreview", () => {
).toBe("[图片]");
});
it("uses image placeholder for a mapped UI image message", () => {
expect(
getSplashLatestMessagePreview({
content: "",
imageUrl: "https://example.com/image.jpg",
}),
).toBe("[图片]");
});
it("uses voice placeholder for voice messages", () => {
expect(
getSplashLatestMessagePreview({
@@ -54,6 +66,35 @@ describe("getSplashLatestMessagePreview", () => {
).toBe("[语音]");
});
it("uses media placeholders for mapped UI lock reasons", () => {
expect(
getSplashLatestMessagePreview({
content: "",
lockReason: "voice_message",
}),
).toBe("[语音]");
expect(
getSplashLatestMessagePreview({
content: "",
lockReason: "image_paywall",
}),
).toBe("[图片]");
});
it("skips synthetic messages when selecting the latest preview", () => {
expect(
getLatestSplashMessagePreview([
{ content: "Real message" },
{ content: "Greeting", isSynthetic: true },
]),
).toBe("Real message");
expect(
getLatestSplashMessagePreview([
{ content: "Greeting", isSynthetic: true },
]),
).toBeNull();
});
it("falls back when the latest message has no displayable content", () => {
expect(
getSplashLatestMessagePreview({
@@ -3,7 +3,8 @@
import { useEffect, useState } from "react";
import type { LoginStatus } from "@/data/dto/auth";
import { fetchSplashLatestMessagePreview } from "@/lib/chat/splash_latest_message";
import { loadSplashLatestMessagePreview } from "@/lib/chat/splash_latest_message";
import { useSplashLatestMessageCache } from "@/providers/splash-latest-message-provider";
import { Logger, Result } from "@/utils";
const log = new Logger("SplashLatestMessage");
@@ -30,6 +31,7 @@ export function useSplashLatestMessage({
isAuthLoading,
loginStatus,
}: UseSplashLatestMessageInput): UseSplashLatestMessageOutput {
const cache = useSplashLatestMessageCache();
const [state, setState] = useState<SplashLatestMessageState>({
key: "",
loaded: false,
@@ -45,7 +47,7 @@ export function useSplashLatestMessage({
let cancelled = false;
const loadLatestMessage = async () => {
const result = await fetchSplashLatestMessagePreview();
const result = await loadSplashLatestMessagePreview({ cache });
if (cancelled) return;
if (Result.isErr(result)) {
@@ -69,7 +71,7 @@ export function useSplashLatestMessage({
return () => {
cancelled = true;
};
}, [requestKey, shouldLoad]);
}, [cache, requestKey, shouldLoad]);
if (!shouldLoad) return { message: null, isLoading: false };
if (state.key !== requestKey || !state.loaded) {