fix(chat): stabilize message identities

This commit is contained in:
2026-07-20 18:30:21 +08:00
parent 159e8bfd59
commit 1e13f94b5d
33 changed files with 718 additions and 212 deletions
@@ -10,12 +10,13 @@ import {
describe("chat area render items", () => {
it("keeps local message keys stable when history is prepended", () => {
const localMessage: UiMessage = {
displayId: "client:message:local-1",
content: "optimistic message",
isFromAI: false,
date: "2026-07-08",
};
const historyMessage: UiMessage = {
id: "history-msg-1",
displayId: "history-msg-1",
content: "history message",
isFromAI: true,
date: "2026-07-07",
@@ -28,12 +29,34 @@ describe("chat area render items", () => {
getMessageKey,
);
expect(findMessageKey(firstItems, localMessage)).toBe("local-msg-1");
expect(findMessageKey(secondItems, localMessage)).toBe("local-msg-1");
expect(findMessageKey(firstItems, localMessage)).toBe(
"msg-client:message:local-1",
);
expect(findMessageKey(secondItems, localMessage)).toBe(
"msg-client:message:local-1",
);
expect(findMessageKey(secondItems, historyMessage)).toBe(
"msg-history-msg-1",
);
});
it("derives the same key after a message object is recreated", () => {
const getMessageKey = createChatMessageKeyResolver();
const original: UiMessage = {
displayId: "server:message-1:assistant",
remoteId: "message-1",
content: "Original",
isFromAI: true,
date: "2026-07-20",
};
const updated: UiMessage = {
...original,
content: "Unlocked",
locked: false,
};
expect(getMessageKey(updated)).toBe(getMessageKey(original));
});
});
function findMessageKey(
@@ -9,6 +9,7 @@ import { createChatPromotionState } from "@/stores/chat/helper/promotion";
import {
resolveChatUnlockReturnUrl,
resolveMessageUnlockRequest,
resolvePendingUnlockDisplayMessageId,
shouldResumePendingChatUnlock,
} from "../hooks/use-chat-unlock-coordinator";
@@ -31,11 +32,15 @@ describe("chat unlock coordinator", () => {
it("creates a regular message unlock request with its remote id", () => {
expect(
resolveMessageUnlockRequest(
{ messageId: "message-1", kind: "private" },
{
displayMessageId: "server:message-1:assistant",
remoteMessageId: "message-1",
kind: "private",
},
defaultScope,
),
).toEqual({
displayMessageId: "message-1",
displayMessageId: "server:message-1:assistant",
messageId: "message-1",
kind: "private",
returnUrl: "/chat",
@@ -51,7 +56,11 @@ describe("chat unlock coordinator", () => {
expect(
resolveMessageUnlockRequest(
{ messageId: "image-1", kind: "image" },
{
displayMessageId: "image-1",
remoteMessageId: "image-1",
kind: "image",
},
imageScope,
).returnUrl,
).toBe("/chat?image=image-1");
@@ -62,7 +71,10 @@ describe("chat unlock coordinator", () => {
expect(
resolveMessageUnlockRequest(
{ messageId: "promotion:promotion-1", kind: "image" },
{
displayMessageId: "promotion:promotion-1",
kind: "image",
},
{ ...defaultScope, promotion },
),
).toEqual({
@@ -83,7 +95,11 @@ describe("chat unlock coordinator", () => {
expect(
resolveMessageUnlockRequest(
{ messageId: "promotion-message-1", kind: "image" },
{
displayMessageId: "promotion:promotion-1",
remoteMessageId: "promotion-message-1",
kind: "image",
},
{ ...defaultScope, promotion },
).messageId,
).toBe("promotion-message-1");
@@ -103,6 +119,40 @@ describe("chat unlock coordinator", () => {
).toBe(true);
});
it("maps a legacy pending remote id to the current display identity", () => {
const pending = createPendingUnlock({
displayMessageId: "message-1",
messageId: "message-1",
kind: "private",
returnUrl: "/chat",
});
expect(
resolvePendingUnlockDisplayMessageId(
pending,
[
{
displayId: "server:message-1:user",
remoteId: "message-1",
content: "Question",
isFromAI: false,
date: "2026-07-20",
},
{
displayId: "server:message-1:assistant",
remoteId: "message-1",
content: "Locked reply",
isFromAI: true,
date: "2026-07-20",
locked: true,
lockReason: "private_message",
},
],
null,
),
).toBe("server:message-1:assistant");
});
it("only resumes an image return when its message matches the overlay", () => {
const imageScope = {
defaultReturnUrl: "/chat",
@@ -136,7 +186,7 @@ describe("chat unlock coordinator", () => {
expect(
resolveMessageUnlockRequest(
{ messageId: imageMessageId, kind: "image" },
{ displayMessageId: imageMessageId, kind: "image" },
{
...imageScope,
promotion,
+1 -14
View File
@@ -7,20 +7,7 @@ export type ChatRenderItem =
| { type: "msg"; message: UiMessage; key: string };
export function createChatMessageKeyResolver(): ChatMessageKeyResolver {
const localMessageKeys = new WeakMap<UiMessage, string>();
let nextLocalMessageKey = 0;
return (message) => {
if (message.id && message.id.length > 0) return `msg-${message.id}`;
const existing = localMessageKeys.get(message);
if (existing) return existing;
nextLocalMessageKey += 1;
const next = `local-msg-${nextLocalMessageKey}`;
localMessageKeys.set(message, next);
return next;
};
return (message) => `msg-${message.displayId}`;
}
export function buildChatRenderItems(
+49 -20
View File
@@ -89,7 +89,10 @@ export function ChatScreen() {
() =>
imageMessageId
? visibleMessages.find(
(item) => item.id === imageMessageId && item.imageUrl,
(item) =>
(item.displayId === imageMessageId ||
item.remoteId === imageMessageId) &&
item.imageUrl,
) ?? null
: null,
[imageMessageId, visibleMessages],
@@ -175,24 +178,46 @@ export function ChatScreen() {
state.characterErrorCode,
]);
function handleUnlockPrivateMessage(messageId: string): void {
unlockCoordinator.requestMessageUnlock(messageId, "private");
}
function handleUnlockVoiceMessage(messageId: string): void {
unlockCoordinator.requestMessageUnlock(messageId, "voice");
}
function handleUnlockImageMessage(messageId: string): void {
unlockCoordinator.requestMessageUnlock(messageId, "image");
}
function handleOpenImage(messageId: string): void {
router.push(buildChatImageOverlayUrl(messageId, characterRoutes.chat), {
scroll: false,
function handleUnlockPrivateMessage(
displayMessageId: string,
remoteMessageId?: string,
): void {
unlockCoordinator.requestMessageUnlock({
displayMessageId,
remoteMessageId,
kind: "private",
});
}
function handleUnlockVoiceMessage(
displayMessageId: string,
remoteMessageId?: string,
): void {
unlockCoordinator.requestMessageUnlock({
displayMessageId,
remoteMessageId,
kind: "voice",
});
}
function handleUnlockImageMessage(
displayMessageId: string,
remoteMessageId?: string,
): void {
unlockCoordinator.requestMessageUnlock({
displayMessageId,
remoteMessageId,
kind: "image",
});
}
function handleOpenImage(displayMessageId: string): void {
router.push(
buildChatImageOverlayUrl(displayMessageId, characterRoutes.chat),
{ scroll: false },
);
}
function handleCloseImageViewer(): void {
router.replace(
buildChatWithoutImageOverlayUrl(searchParams, characterRoutes.chat),
@@ -201,8 +226,12 @@ export function ChatScreen() {
}
function handleUnlockImagePaywall(): void {
if (!imageMessageId) return;
unlockCoordinator.requestMessageUnlock(imageMessageId, "image");
if (!selectedImageMessage) return;
unlockCoordinator.requestMessageUnlock({
displayMessageId: selectedImageMessage.displayId,
remoteMessageId: selectedImageMessage.remoteId,
kind: "image",
});
}
return (
@@ -279,12 +308,12 @@ export function ChatScreen() {
{selectedImageMessage?.imageUrl ? (
<FullscreenImageViewer
characterId={state.characterId}
messageId={selectedImageMessage.id}
remoteMessageId={selectedImageMessage.remoteId}
imageUrl={selectedImageMessage.imageUrl}
imagePaywalled={selectedImageMessage.imagePaywalled === true}
isUnlockingImagePaywall={
state.isUnlockingMessage &&
state.unlockingMessageId === selectedImageMessage.id
state.unlockingMessageId === selectedImageMessage.displayId
}
onUnlockImagePaywall={handleUnlockImagePaywall}
onClose={handleCloseImageViewer}
@@ -265,19 +265,19 @@ function createTouchEvent(
return event;
}
function createMessage(id: string): UiMessage {
function createMessage(displayId: string): UiMessage {
return {
id,
content: `Message ${id}`,
displayId,
content: `Message ${displayId}`,
isFromAI: true,
date: "2026-07-15",
};
}
function createUserMessage(id: string): UiMessage {
function createUserMessage(displayId: string): UiMessage {
return {
id,
content: `Message ${id}`,
displayId,
content: `Message ${displayId}`,
isFromAI: false,
date: "2026-07-15",
};
@@ -128,7 +128,8 @@ describe("chat Tailwind components", () => {
const openableHtml = renderToStaticMarkup(
<ImageBubble
characterId="elio"
messageId="message-1"
displayMessageId="server:message-1:assistant"
remoteMessageId="message-1"
imageUrl="/chat-image.png"
onOpenImage={() => undefined}
/>,
@@ -136,6 +137,7 @@ describe("chat Tailwind components", () => {
const paywalledHtml = renderToStaticMarkup(
<ImageBubble
characterId="elio"
displayMessageId="server:message-2:assistant"
imageUrl="/locked-image.png"
imagePaywalled
/>,
+16 -10
View File
@@ -54,13 +54,18 @@ export interface ChatAreaProps {
isLoadingMoreHistory?: boolean;
isUnlockingMessage?: boolean;
unlockingMessageId?: string | null;
onUnlockPrivateMessage?: (messageId: string) => void;
onUnlockVoiceMessage?: (messageId: string) => void;
onUnlockImageMessage?: (messageId: string) => void;
onOpenImage?: (messageId: string) => void;
onUnlockPrivateMessage?: ChatMessageAction;
onUnlockVoiceMessage?: ChatMessageAction;
onUnlockImageMessage?: ChatMessageAction;
onOpenImage?: (displayMessageId: string) => void;
onLoadMoreHistory?: () => void;
}
type ChatMessageAction = (
displayMessageId: string,
remoteMessageId?: string,
) => void;
export function ChatArea({
characterId,
messages,
@@ -278,10 +283,10 @@ function renderMessagesWithDateHeaders(
getMessageKey: ChatMessageKeyResolver,
isUnlockingMessage?: boolean,
unlockingMessageId?: string | null,
onUnlockPrivateMessage?: (messageId: string) => void,
onUnlockVoiceMessage?: (messageId: string) => void,
onUnlockImageMessage?: (messageId: string) => void,
onOpenImage?: (messageId: string) => void,
onUnlockPrivateMessage?: ChatMessageAction,
onUnlockVoiceMessage?: ChatMessageAction,
onUnlockImageMessage?: ChatMessageAction,
onOpenImage?: (displayMessageId: string) => void,
) {
return buildChatRenderItems(messages, getMessageKey).map((item) =>
item.type === "date" ? (
@@ -290,7 +295,8 @@ function renderMessagesWithDateHeaders(
<MessageBubble
characterId={characterId}
key={item.key}
messageId={item.message.id}
displayMessageId={item.message.displayId}
remoteMessageId={item.message.remoteId}
content={item.message.content}
imageUrl={item.message.imageUrl}
imagePaywalled={item.message.imagePaywalled}
@@ -302,7 +308,7 @@ function renderMessagesWithDateHeaders(
privateMessageHint={item.message.privateMessageHint}
isUnlockingMessage={
isUnlockingMessage === true &&
item.message.id === unlockingMessageId
item.message.displayId === unlockingMessageId
}
onUnlockPrivateMessage={onUnlockPrivateMessage}
onUnlockVoiceMessage={onUnlockVoiceMessage}
+3 -3
View File
@@ -13,7 +13,7 @@ import { useCachedChatMediaUrl } from "@/lib/chat/use_cached_chat_media_url";
export interface ChatMediaImageProps {
characterId: string;
messageId?: string;
remoteMessageId?: string;
remoteUrl: string;
alt?: string;
className?: string;
@@ -30,7 +30,7 @@ export interface ChatMediaImageProps {
export function ChatMediaImage({
characterId,
messageId,
remoteMessageId,
remoteUrl,
alt = "",
className,
@@ -48,7 +48,7 @@ export function ChatMediaImage({
const { mediaUrl, isUsingCachedMedia, reportMediaError } =
useCachedChatMediaUrl({
characterId,
messageId,
messageId: remoteMessageId,
remoteUrl,
kind: "image",
});
@@ -19,7 +19,7 @@ import styles from "./fullscreen-image-viewer.module.css";
export interface FullscreenImageViewerProps {
characterId: string;
messageId?: string;
remoteMessageId?: string;
imageUrl: string;
imagePaywalled?: boolean;
isUnlockingImagePaywall?: boolean;
@@ -29,7 +29,7 @@ export interface FullscreenImageViewerProps {
export function FullscreenImageViewer({
characterId,
messageId,
remoteMessageId,
imageUrl,
imagePaywalled = false,
isUnlockingImagePaywall = false,
@@ -54,7 +54,7 @@ export function FullscreenImageViewer({
>
<ChatMediaImage
characterId={characterId}
messageId={messageId}
remoteMessageId={remoteMessageId}
remoteUrl={imageUrl}
className={styles.paywallImage}
nativeClassName={`${styles.nativePaywallImage} ${styles.paywallImage}`}
@@ -104,7 +104,7 @@ export function FullscreenImageViewer({
/>
<ChatMediaImage
characterId={characterId}
messageId={messageId}
remoteMessageId={remoteMessageId}
remoteUrl={imageUrl}
className={styles.viewerImage}
errorClassName="error"
+9 -7
View File
@@ -12,20 +12,22 @@ import { ChatMediaImage } from "./chat-media-image";
export interface ImageBubbleProps {
characterId: string;
messageId?: string;
displayMessageId: string;
remoteMessageId?: string;
imageUrl: string; // base64 data URI 或 URL
imagePaywalled?: boolean;
onOpenImage?: (messageId: string) => void;
onOpenImage?: (displayMessageId: string) => void;
}
export function ImageBubble({
characterId,
messageId,
displayMessageId,
remoteMessageId,
imageUrl,
imagePaywalled = false,
onOpenImage,
}: ImageBubbleProps) {
const canOpen = Boolean(messageId && onOpenImage);
const canOpen = Boolean(onOpenImage);
const imageClassName = [
"block h-auto w-full object-cover",
imagePaywalled ? "scale-104 blur-sm" : "",
@@ -34,8 +36,8 @@ export function ImageBubble({
.join(" ");
const openImage = () => {
if (!messageId || !onOpenImage) return;
onOpenImage(messageId);
if (!onOpenImage) return;
onOpenImage(displayMessageId);
};
return (
@@ -55,7 +57,7 @@ export function ImageBubble({
>
<ChatMediaImage
characterId={characterId}
messageId={messageId}
remoteMessageId={remoteMessageId}
remoteUrl={imageUrl}
className={imageClassName}
errorClassName="flex size-(--chat-media-size,220px) items-center justify-center bg-(--color-bubble-background,#fff) text-(length:--icon-size-xl,24px) text-(--color-text-secondary,#9e9e9e)"
+19 -10
View File
@@ -16,7 +16,8 @@ import styles from "./chat-area.module.css";
export interface MessageBubbleProps {
characterId: string;
messageId?: string;
displayMessageId: string;
remoteMessageId?: string;
content: string;
imageUrl?: string | null;
imagePaywalled?: boolean;
@@ -27,15 +28,21 @@ export interface MessageBubbleProps {
lockedPrivate?: boolean | null;
privateMessageHint?: string | null;
isUnlockingMessage?: boolean;
onUnlockPrivateMessage?: (messageId: string) => void;
onUnlockVoiceMessage?: (messageId: string) => void;
onUnlockImageMessage?: (messageId: string) => void;
onOpenImage?: (messageId: string) => void;
onUnlockPrivateMessage?: ChatMessageAction;
onUnlockVoiceMessage?: ChatMessageAction;
onUnlockImageMessage?: ChatMessageAction;
onOpenImage?: (displayMessageId: string) => void;
}
type ChatMessageAction = (
displayMessageId: string,
remoteMessageId?: string,
) => void;
export function MessageBubble({
characterId,
messageId,
displayMessageId,
remoteMessageId,
content,
imageUrl,
imagePaywalled,
@@ -57,18 +64,19 @@ export function MessageBubble({
return (
<div
className={styles.bubbleRowAi}
data-chat-message-id={messageId}
data-chat-message-id={displayMessageId}
aria-label="AI message"
>
<MessageAvatar isFromAI={true} />
<div className={styles.bubbleInlineSpacer} aria-hidden="true" />
<MessageContent
characterId={characterId}
displayMessageId={displayMessageId}
remoteMessageId={remoteMessageId}
content={content}
imageUrl={imageUrl}
imagePaywalled={imagePaywalled}
audioUrl={audioUrl}
messageId={messageId}
isFromAI={true}
locked={locked}
lockReason={lockReason}
@@ -88,17 +96,18 @@ export function MessageBubble({
return (
<div
className={styles.bubbleRowUser}
data-chat-message-id={messageId}
data-chat-message-id={displayMessageId}
aria-label="User message"
>
<div className={styles.bubbleAvatarSpacer} aria-hidden="true" />
<MessageContent
characterId={characterId}
displayMessageId={displayMessageId}
remoteMessageId={remoteMessageId}
content={content}
imageUrl={imageUrl}
imagePaywalled={imagePaywalled}
audioUrl={audioUrl}
messageId={messageId}
isFromAI={false}
locked={locked}
lockReason={lockReason}
+23 -14
View File
@@ -8,32 +8,39 @@ import styles from "./chat-area.module.css";
export interface MessageContentProps {
characterId: string;
displayMessageId: string;
remoteMessageId?: string;
content: string;
imageUrl?: string | null;
imagePaywalled?: boolean;
audioUrl?: string | null;
messageId?: string;
isFromAI: boolean;
locked?: boolean | null;
lockReason?: string | null;
lockedPrivate?: boolean | null;
privateMessageHint?: string | null;
isUnlockingMessage?: boolean;
onUnlockPrivateMessage?: (messageId: string) => void;
onUnlockVoiceMessage?: (messageId: string) => void;
onUnlockImageMessage?: (messageId: string) => void;
onOpenImage?: (messageId: string) => void;
onUnlockPrivateMessage?: ChatMessageAction;
onUnlockVoiceMessage?: ChatMessageAction;
onUnlockImageMessage?: ChatMessageAction;
onOpenImage?: (displayMessageId: string) => void;
}
type ChatMessageAction = (
displayMessageId: string,
remoteMessageId?: string,
) => void;
const IMAGE_PLACEHOLDER = "[图片]";
export function MessageContent({
characterId,
displayMessageId,
remoteMessageId,
content,
imageUrl,
imagePaywalled,
audioUrl,
messageId,
isFromAI,
locked,
lockReason,
@@ -57,16 +64,17 @@ export function MessageContent({
(lockReason === "image_paywall" || lockReason === "image");
const shouldRenderVoiceMessage = hasAudio || isLockedVoiceMessage;
const handleUnlockPrivateMessage =
messageId && onUnlockPrivateMessage
? () => onUnlockPrivateMessage(messageId)
onUnlockPrivateMessage
? () =>
onUnlockPrivateMessage(displayMessageId, remoteMessageId)
: undefined;
const handleUnlockVoiceMessage =
isLockedVoiceMessage && messageId && onUnlockVoiceMessage
? () => onUnlockVoiceMessage(messageId)
isLockedVoiceMessage && onUnlockVoiceMessage
? () => onUnlockVoiceMessage(displayMessageId, remoteMessageId)
: undefined;
const handleUnlockImageMessage =
isLockedImageMessage && messageId && onUnlockImageMessage
? () => onUnlockImageMessage(messageId)
isLockedImageMessage && onUnlockImageMessage
? () => onUnlockImageMessage(displayMessageId, remoteMessageId)
: undefined;
return (
@@ -93,7 +101,8 @@ export function MessageContent({
{hasImage && imageUrl && (
<ImageBubble
characterId={characterId}
messageId={messageId}
displayMessageId={displayMessageId}
remoteMessageId={remoteMessageId}
imageUrl={imageUrl}
imagePaywalled={imagePaywalled}
onOpenImage={onOpenImage}
@@ -102,7 +111,7 @@ export function MessageContent({
{shouldRenderVoiceMessage ? (
<VoiceBubble
characterId={characterId}
messageId={messageId}
remoteMessageId={remoteMessageId}
audioUrl={audioUrl}
isFromAI={isFromAI}
locked={isLockedVoiceMessage}
+3 -3
View File
@@ -9,7 +9,7 @@ import styles from "./voice-bubble.module.css";
export interface VoiceBubbleProps {
characterId: string;
messageId?: string;
remoteMessageId?: string;
audioUrl?: string | null;
isFromAI: boolean;
locked?: boolean;
@@ -20,7 +20,7 @@ export interface VoiceBubbleProps {
export function VoiceBubble({
characterId,
messageId,
remoteMessageId,
audioUrl,
isFromAI,
locked = false,
@@ -36,7 +36,7 @@ export function VoiceBubble({
const { mediaUrl, isUsingCachedMedia, reportMediaError } =
useCachedChatMediaUrl({
characterId,
messageId,
messageId: remoteMessageId,
remoteUrl: audioUrl,
kind: "audio",
});
@@ -19,6 +19,7 @@ import {
} from "@/stores/chat/chat-context";
import type { ChatPromotionState } from "@/stores/chat/helper/promotion";
import type { ChatUnlockPaywallRequest } from "@/stores/chat/chat-state";
import type { UiMessage } from "@/stores/chat/ui-message";
import { useUserSelector } from "@/stores/user/user-context";
import { getInsufficientCreditsSubscriptionType } from "../chat-screen.helpers";
@@ -60,10 +61,11 @@ export interface UseChatUnlockCoordinatorInput
}
export interface UseChatUnlockCoordinatorOutput {
requestMessageUnlock: (
messageId: string,
kind: PendingChatUnlockKind,
) => void;
requestMessageUnlock: (input: {
displayMessageId: string;
remoteMessageId?: string;
kind: PendingChatUnlockKind;
}) => void;
dialogs: ChatUnlockDialogModel;
}
@@ -81,6 +83,7 @@ export function useChatUnlockCoordinator({
(state) => ({
characterId: state.context.characterId,
historyLoaded: state.context.historyLoaded,
messages: state.context.messages,
isUnlockingHistory: state.matches({ userSession: "unlockingHistory" }),
lockedHistoryCount: state.context.lockedHistoryCount,
unlockHistoryError: state.context.unlockHistoryError,
@@ -141,10 +144,15 @@ export function useChatUnlockCoordinator({
const consumed = await consumePendingChatUnlock(chatState.characterId);
if (cancelled || !consumed) return;
const displayMessageId = resolvePendingUnlockDisplayMessageId(
consumed,
chatState.messages,
promotion,
);
chatDispatch({
type: "ChatUnlockMessageRequested",
messageId: consumed.displayMessageId,
displayMessageId,
remoteMessageId: consumed.messageId,
kind: consumed.kind,
lockType: consumed.lockType,
@@ -165,14 +173,17 @@ export function useChatUnlockCoordinator({
imageMessageId,
imageReturnUrl,
navigator.isAuthenticatedUser,
promotion,
chatState.messages,
]);
function requestMessageUnlock(
messageId: string,
kind: PendingChatUnlockKind,
): void {
function requestMessageUnlock(input: {
displayMessageId: string;
remoteMessageId?: string;
kind: PendingChatUnlockKind;
}): void {
const request = resolveMessageUnlockRequest(
{ messageId, kind },
input,
{
defaultReturnUrl,
imageMessageId,
@@ -186,7 +197,7 @@ export function useChatUnlockCoordinator({
onAuthenticated: () => {
chatDispatch({
type: "ChatUnlockMessageRequested",
messageId: request.displayMessageId,
displayMessageId: request.displayMessageId,
remoteMessageId: request.messageId,
kind: request.kind,
lockType: request.lockType,
@@ -258,23 +269,21 @@ export function useChatUnlockCoordinator({
export function resolveMessageUnlockRequest(
input: {
messageId: string;
displayMessageId: string;
remoteMessageId?: string;
kind: PendingChatUnlockKind;
},
scope: ChatUnlockCoordinatorScope,
): CoordinatedMessageUnlockRequest {
const matchedPromotion =
scope.promotion?.message.id === input.messageId
scope.promotion?.message.displayId === input.displayMessageId
? scope.promotion
: null;
const temporaryPromotionMessageId = matchedPromotion
? `promotion:${matchedPromotion.session.clientLockId}`
: null;
const remoteMessageId =
input.remoteMessageId ?? matchedPromotion?.message.remoteId;
const target = {
displayMessageId: input.messageId,
...(input.messageId !== temporaryPromotionMessageId
? { messageId: input.messageId }
: {}),
displayMessageId: input.displayMessageId,
...(remoteMessageId ? { messageId: remoteMessageId } : {}),
kind: input.kind,
...(matchedPromotion
? {
@@ -300,6 +309,40 @@ export function shouldResumePendingChatUnlock(
return isCurrentImageUnlock(pending, scope.imageMessageId);
}
export function resolvePendingUnlockDisplayMessageId(
pending: PendingChatUnlock,
messages: readonly UiMessage[],
promotion: ChatPromotionState | null,
): string {
if (
promotion &&
(promotion.message.displayId === pending.displayMessageId ||
(pending.messageId &&
promotion.message.remoteId === pending.messageId))
) {
return promotion.message.displayId;
}
const exactMessage = messages.find(
(message) => message.displayId === pending.displayMessageId,
);
if (exactMessage) return exactMessage.displayId;
if (!pending.messageId) return pending.displayMessageId;
const remoteMessage =
messages.find(
(message) =>
message.isFromAI &&
message.locked === true &&
message.remoteId === pending.messageId,
) ??
messages.find(
(message) =>
message.isFromAI && message.remoteId === pending.messageId,
);
return remoteMessage?.displayId ?? pending.displayMessageId;
}
export function resolveChatUnlockReturnUrl(
target: Pick<
CoordinatedMessageUnlockRequest,