fix(chat): apply unlocked voice audio url
This commit is contained in:
@@ -47,6 +47,7 @@ export function MessageContent({
|
|||||||
lockedPrivate === true ||
|
lockedPrivate === true ||
|
||||||
(locked === true && lockReason === "private_message");
|
(locked === true && lockReason === "private_message");
|
||||||
const isLockedVoiceMessage = locked === true && lockReason === "voice_message";
|
const isLockedVoiceMessage = locked === true && lockReason === "voice_message";
|
||||||
|
const shouldRenderVoiceMessage = hasAudio || isLockedVoiceMessage;
|
||||||
const handleUnlockPrivateMessage =
|
const handleUnlockPrivateMessage =
|
||||||
messageId && onUnlockPrivateMessage
|
messageId && onUnlockPrivateMessage
|
||||||
? () => onUnlockPrivateMessage(messageId)
|
? () => onUnlockPrivateMessage(messageId)
|
||||||
@@ -79,7 +80,7 @@ export function MessageContent({
|
|||||||
onOpenImage={onOpenImage}
|
onOpenImage={onOpenImage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{hasAudio && audioUrl ? (
|
{shouldRenderVoiceMessage ? (
|
||||||
<VoiceBubble
|
<VoiceBubble
|
||||||
messageId={messageId}
|
messageId={messageId}
|
||||||
audioUrl={audioUrl}
|
audioUrl={audioUrl}
|
||||||
@@ -90,7 +91,7 @@ export function MessageContent({
|
|||||||
onUnlock={handleUnlockVoiceMessage}
|
onUnlock={handleUnlockVoiceMessage}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{hasText && !hasAudio ? (
|
{hasText && !shouldRenderVoiceMessage ? (
|
||||||
<TextBubble content={content} isFromAI={isFromAI} />
|
<TextBubble content={content} isFromAI={isFromAI} />
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import styles from "./voice-bubble.module.css";
|
|||||||
|
|
||||||
export interface VoiceBubbleProps {
|
export interface VoiceBubbleProps {
|
||||||
messageId?: string;
|
messageId?: string;
|
||||||
audioUrl: string;
|
audioUrl?: string | null;
|
||||||
isFromAI: boolean;
|
isFromAI: boolean;
|
||||||
locked?: boolean;
|
locked?: boolean;
|
||||||
hint?: string | null;
|
hint?: string | null;
|
||||||
@@ -28,6 +28,7 @@ export function VoiceBubble({
|
|||||||
const audioRef = useRef<HTMLAudioElement>(null);
|
const audioRef = useRef<HTMLAudioElement>(null);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [errorMediaUrl, setErrorMediaUrl] = useState<string | null>(null);
|
const [errorMediaUrl, setErrorMediaUrl] = useState<string | null>(null);
|
||||||
|
const hasAudio = audioUrl != null && audioUrl.length > 0;
|
||||||
const { mediaUrl, isUsingCachedMedia, reportMediaError } =
|
const { mediaUrl, isUsingCachedMedia, reportMediaError } =
|
||||||
useCachedChatMediaUrl({
|
useCachedChatMediaUrl({
|
||||||
messageId,
|
messageId,
|
||||||
@@ -66,7 +67,7 @@ export function VoiceBubble({
|
|||||||
const hasError = errorMediaUrl === mediaUrl;
|
const hasError = errorMediaUrl === mediaUrl;
|
||||||
|
|
||||||
const handleToggle = () => {
|
const handleToggle = () => {
|
||||||
if (locked) return;
|
if (locked || !hasAudio) return;
|
||||||
|
|
||||||
const audio = audioRef.current;
|
const audio = audioRef.current;
|
||||||
if (!audio || hasError) return;
|
if (!audio || hasError) return;
|
||||||
@@ -92,7 +93,7 @@ export function VoiceBubble({
|
|||||||
type="button"
|
type="button"
|
||||||
className={styles.playButton}
|
className={styles.playButton}
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
disabled={locked || hasError}
|
disabled={locked || hasError || !hasAudio}
|
||||||
aria-label={isPlaying ? "Pause voice message" : "Play voice message"}
|
aria-label={isPlaying ? "Pause voice message" : "Play voice message"}
|
||||||
>
|
>
|
||||||
{locked ? (
|
{locked ? (
|
||||||
@@ -132,7 +133,7 @@ export function VoiceBubble({
|
|||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{!locked ? (
|
{!locked && hasAudio ? (
|
||||||
<audio ref={audioRef} src={mediaUrl} preload="metadata" />
|
<audio ref={audioRef} src={mediaUrl} preload="metadata" />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ describe("UnlockPrivateResponse", () => {
|
|||||||
const response = UnlockPrivateResponse.from({
|
const response = UnlockPrivateResponse.from({
|
||||||
unlocked: true,
|
unlocked: true,
|
||||||
content: "完整消息内容",
|
content: "完整消息内容",
|
||||||
|
audioUrl: "https://example.com/unlocked-voice.mp3",
|
||||||
reason: "ok",
|
reason: "ok",
|
||||||
creditBalance: 9180,
|
creditBalance: 9180,
|
||||||
creditsCharged: 10,
|
creditsCharged: 10,
|
||||||
@@ -24,6 +25,7 @@ describe("UnlockPrivateResponse", () => {
|
|||||||
|
|
||||||
expect(response.unlocked).toBe(true);
|
expect(response.unlocked).toBe(true);
|
||||||
expect(response.content).toBe("完整消息内容");
|
expect(response.content).toBe("完整消息内容");
|
||||||
|
expect(response.audioUrl).toBe("https://example.com/unlocked-voice.mp3");
|
||||||
expect(response.reason).toBe("ok");
|
expect(response.reason).toBe("ok");
|
||||||
expect(response.creditBalance).toBe(9180);
|
expect(response.creditBalance).toBe(9180);
|
||||||
expect(response.creditsCharged).toBe(10);
|
expect(response.creditsCharged).toBe(10);
|
||||||
@@ -66,6 +68,7 @@ describe("UnlockPrivateResponse", () => {
|
|||||||
const response = UnlockPrivateResponse.from({
|
const response = UnlockPrivateResponse.from({
|
||||||
unlocked: false,
|
unlocked: false,
|
||||||
content: null,
|
content: null,
|
||||||
|
audioUrl: null,
|
||||||
reason: "insufficient_balance",
|
reason: "insufficient_balance",
|
||||||
lockDetail: {
|
lockDetail: {
|
||||||
locked: true,
|
locked: true,
|
||||||
@@ -78,6 +81,7 @@ describe("UnlockPrivateResponse", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(response.content).toBe("");
|
expect(response.content).toBe("");
|
||||||
|
expect(response.audioUrl).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("defaults nullable credit fields to zero", () => {
|
it("defaults nullable credit fields to zero", () => {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type { ChatLockDetailData } from "@/data/schemas/chat";
|
|||||||
export class UnlockPrivateResponse {
|
export class UnlockPrivateResponse {
|
||||||
declare readonly unlocked: boolean;
|
declare readonly unlocked: boolean;
|
||||||
declare readonly content: string;
|
declare readonly content: string;
|
||||||
|
declare readonly audioUrl: string;
|
||||||
declare readonly reason: UnlockPrivateReason;
|
declare readonly reason: UnlockPrivateReason;
|
||||||
declare readonly creditBalance: number;
|
declare readonly creditBalance: number;
|
||||||
declare readonly creditsCharged: number;
|
declare readonly creditsCharged: number;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { ChatLockDetailData } from "@/data/schemas/chat";
|
|
||||||
import { ChatMessage } from "@/data/dto/chat";
|
import { ChatMessage } from "@/data/dto/chat";
|
||||||
|
import type { UnlockedPrivateMessageLocalPatch } from "@/data/repositories/interfaces";
|
||||||
import { LocalChatStorage, LocalMessage } from "@/data/storage/chat";
|
import { LocalChatStorage, LocalMessage } from "@/data/storage/chat";
|
||||||
import { Result } from "@/utils";
|
import { Result } from "@/utils";
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ export class ChatLocalMessageStore {
|
|||||||
|
|
||||||
async markMessageUnlocked(
|
async markMessageUnlocked(
|
||||||
messageId: string,
|
messageId: string,
|
||||||
lockDetail?: ChatLockDetailData,
|
patch: UnlockedPrivateMessageLocalPatch = {},
|
||||||
): Promise<Result<void>> {
|
): Promise<Result<void>> {
|
||||||
return Result.wrap(async () => {
|
return Result.wrap(async () => {
|
||||||
const localResult = await this.getMessages();
|
const localResult = await this.getMessages();
|
||||||
@@ -24,7 +24,9 @@ export class ChatLocalMessageStore {
|
|||||||
changed = true;
|
changed = true;
|
||||||
return ChatMessage.from({
|
return ChatMessage.from({
|
||||||
...message.toJson(),
|
...message.toJson(),
|
||||||
lockDetail: lockDetail ?? {
|
content: normalizeUnlockedContent(patch.content, message.content),
|
||||||
|
audioUrl: normalizeUnlockedAudioUrl(patch.audioUrl, message.audioUrl),
|
||||||
|
lockDetail: patch.lockDetail ?? {
|
||||||
...message.lockDetail,
|
...message.lockDetail,
|
||||||
locked: false,
|
locked: false,
|
||||||
showContent: true,
|
showContent: true,
|
||||||
@@ -105,6 +107,22 @@ export class ChatLocalMessageStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeUnlockedContent(
|
||||||
|
nextContent: string | undefined,
|
||||||
|
currentContent: string,
|
||||||
|
): string {
|
||||||
|
if (nextContent == null || nextContent.length === 0) return currentContent;
|
||||||
|
return nextContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeUnlockedAudioUrl(
|
||||||
|
nextAudioUrl: string | null | undefined,
|
||||||
|
currentAudioUrl: string | null,
|
||||||
|
): string | null {
|
||||||
|
if (nextAudioUrl == null || nextAudioUrl.length === 0) return currentAudioUrl;
|
||||||
|
return nextAudioUrl;
|
||||||
|
}
|
||||||
|
|
||||||
function shouldMarkMessageUnlocked(
|
function shouldMarkMessageUnlocked(
|
||||||
message: ChatMessage,
|
message: ChatMessage,
|
||||||
messageId: string,
|
messageId: string,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import type {
|
|||||||
UnlockHistoryResponse,
|
UnlockHistoryResponse,
|
||||||
UnlockPrivateResponse,
|
UnlockPrivateResponse,
|
||||||
} from "@/data/dto/chat";
|
} from "@/data/dto/chat";
|
||||||
import type { ChatLockDetailData } from "@/data/schemas/chat";
|
|
||||||
import { chatApi } from "@/data/services/api";
|
import { chatApi } from "@/data/services/api";
|
||||||
import {
|
import {
|
||||||
LocalChatMediaStorage,
|
LocalChatMediaStorage,
|
||||||
@@ -18,6 +17,7 @@ import type {
|
|||||||
CacheRemoteChatMediaInput,
|
CacheRemoteChatMediaInput,
|
||||||
ChatMediaLookupInput,
|
ChatMediaLookupInput,
|
||||||
IChatRepository,
|
IChatRepository,
|
||||||
|
UnlockedPrivateMessageLocalPatch,
|
||||||
} from "@/data/repositories/interfaces";
|
} from "@/data/repositories/interfaces";
|
||||||
import type { Result } from "@/utils";
|
import type { Result } from "@/utils";
|
||||||
|
|
||||||
@@ -66,11 +66,11 @@ export class ChatRepository implements IChatRepository {
|
|||||||
/** 把本地缓存中的单条锁定消息标记为已解锁。 */
|
/** 把本地缓存中的单条锁定消息标记为已解锁。 */
|
||||||
async markPrivateMessageUnlockedInLocal(
|
async markPrivateMessageUnlockedInLocal(
|
||||||
messageId: string,
|
messageId: string,
|
||||||
lockDetail?: ChatLockDetailData,
|
patch?: UnlockedPrivateMessageLocalPatch,
|
||||||
): Promise<Result<void>> {
|
): Promise<Result<void>> {
|
||||||
return this.localMessages.markMessageUnlocked(
|
return this.localMessages.markMessageUnlocked(
|
||||||
messageId,
|
messageId,
|
||||||
lockDetail,
|
patch,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ export interface CacheRemoteChatMediaInput extends ChatMediaLookupInput {
|
|||||||
remoteUrl: string;
|
remoteUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UnlockedPrivateMessageLocalPatch {
|
||||||
|
lockDetail?: ChatLockDetailData;
|
||||||
|
audioUrl?: string | null;
|
||||||
|
content?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IChatRepository {
|
export interface IChatRepository {
|
||||||
/** 发送一条消息。 */
|
/** 发送一条消息。 */
|
||||||
sendMessage(
|
sendMessage(
|
||||||
@@ -51,7 +57,7 @@ export interface IChatRepository {
|
|||||||
/** 把本地缓存中的单条锁定消息标记为已解锁。 */
|
/** 把本地缓存中的单条锁定消息标记为已解锁。 */
|
||||||
markPrivateMessageUnlockedInLocal(
|
markPrivateMessageUnlockedInLocal(
|
||||||
messageId: string,
|
messageId: string,
|
||||||
lockDetail?: ChatLockDetailData,
|
patch?: UnlockedPrivateMessageLocalPatch,
|
||||||
): Promise<Result<void>>;
|
): Promise<Result<void>>;
|
||||||
|
|
||||||
/** 把一条消息写入本地存储。 */
|
/** 把一条消息写入本地存储。 */
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const UnlockPrivateReasonSchema = stringOrEmpty;
|
|||||||
export const UnlockPrivateResponseSchema = z.object({
|
export const UnlockPrivateResponseSchema = z.object({
|
||||||
unlocked: booleanOrFalse,
|
unlocked: booleanOrFalse,
|
||||||
content: stringOrEmpty,
|
content: stringOrEmpty,
|
||||||
|
audioUrl: stringOrEmpty,
|
||||||
reason: UnlockPrivateReasonSchema,
|
reason: UnlockPrivateReasonSchema,
|
||||||
creditBalance: numberOrZero,
|
creditBalance: numberOrZero,
|
||||||
creditsCharged: numberOrZero,
|
creditsCharged: numberOrZero,
|
||||||
|
|||||||
@@ -66,8 +66,7 @@ describe("sendResponseToUiMessage", () => {
|
|||||||
it("maps locked voice messages without treating them as private text", () => {
|
it("maps locked voice messages without treating them as private text", () => {
|
||||||
const message = sendResponseToUiMessage(
|
const message = sendResponseToUiMessage(
|
||||||
makeResponse({
|
makeResponse({
|
||||||
audioUrl:
|
audioUrl: "",
|
||||||
"https://proapi.banlv-ai.com/audio/5198b93c-2915-406a.mp3",
|
|
||||||
lockDetail: {
|
lockDetail: {
|
||||||
locked: true,
|
locked: true,
|
||||||
showContent: false,
|
showContent: false,
|
||||||
@@ -80,9 +79,7 @@ describe("sendResponseToUiMessage", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(message.content).toBe("AI reply");
|
expect(message.content).toBe("AI reply");
|
||||||
expect(message.audioUrl).toBe(
|
expect(message.audioUrl).toBeUndefined();
|
||||||
"https://proapi.banlv-ai.com/audio/5198b93c-2915-406a.mp3",
|
|
||||||
);
|
|
||||||
expect(message.locked).toBe(true);
|
expect(message.locked).toBe(true);
|
||||||
expect(message.lockReason).toBe("voice_message");
|
expect(message.lockReason).toBe("voice_message");
|
||||||
expect(message.lockedPrivate).toBeUndefined();
|
expect(message.lockedPrivate).toBeUndefined();
|
||||||
@@ -243,7 +240,7 @@ describe("localMessagesToUi", () => {
|
|||||||
type: "voice",
|
type: "voice",
|
||||||
content: "hidden voice transcript",
|
content: "hidden voice transcript",
|
||||||
createdAt: "2026-06-25T12:00:00.000Z",
|
createdAt: "2026-06-25T12:00:00.000Z",
|
||||||
audioUrl: "https://example.com/voice.mp3",
|
audioUrl: null,
|
||||||
image: { type: null, url: null },
|
image: { type: null, url: null },
|
||||||
lockDetail: {
|
lockDetail: {
|
||||||
locked: true,
|
locked: true,
|
||||||
@@ -257,7 +254,7 @@ describe("localMessagesToUi", () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
expect(message.content).toBe("hidden voice transcript");
|
expect(message.content).toBe("hidden voice transcript");
|
||||||
expect(message.audioUrl).toBe("https://example.com/voice.mp3");
|
expect(message.audioUrl).toBeUndefined();
|
||||||
expect(message.locked).toBe(true);
|
expect(message.locked).toBe(true);
|
||||||
expect(message.lockReason).toBe("voice_message");
|
expect(message.lockReason).toBe("voice_message");
|
||||||
expect(message.lockedPrivate).toBeUndefined();
|
expect(message.lockedPrivate).toBeUndefined();
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ function makeUnlockPrivateResponse(
|
|||||||
return UnlockPrivateResponse.from({
|
return UnlockPrivateResponse.from({
|
||||||
unlocked: true,
|
unlocked: true,
|
||||||
content: "unlocked content",
|
content: "unlocked content",
|
||||||
|
audioUrl: "",
|
||||||
reason: "ok",
|
reason: "ok",
|
||||||
creditBalance: 90,
|
creditBalance: 90,
|
||||||
creditsCharged: 10,
|
creditsCharged: 10,
|
||||||
@@ -740,7 +741,7 @@ describe("chatMachine transitions", () => {
|
|||||||
actor.stop();
|
actor.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps voice message content unchanged after unlock succeeds", async () => {
|
it("applies the real voice audio url after unlock succeeds", async () => {
|
||||||
const actor = createActor(
|
const actor = createActor(
|
||||||
createTestChatMachine({
|
createTestChatMachine({
|
||||||
historyMessages: [
|
historyMessages: [
|
||||||
@@ -749,7 +750,6 @@ describe("chatMachine transitions", () => {
|
|||||||
content: "Original voice transcript.",
|
content: "Original voice transcript.",
|
||||||
isFromAI: true,
|
isFromAI: true,
|
||||||
date: "2026-06-29",
|
date: "2026-06-29",
|
||||||
audioUrl: "https://example.com/voice.mp3",
|
|
||||||
locked: true,
|
locked: true,
|
||||||
lockReason: "voice_message",
|
lockReason: "voice_message",
|
||||||
privateMessageHint: "A voice message is waiting.",
|
privateMessageHint: "A voice message is waiting.",
|
||||||
@@ -759,6 +759,7 @@ describe("chatMachine transitions", () => {
|
|||||||
messageId: "msg-voice-locked",
|
messageId: "msg-voice-locked",
|
||||||
response: makeUnlockPrivateResponse({
|
response: makeUnlockPrivateResponse({
|
||||||
content: "This response content must be ignored.",
|
content: "This response content must be ignored.",
|
||||||
|
audioUrl: "https://example.com/unlocked-voice.mp3",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -783,7 +784,7 @@ describe("chatMachine transitions", () => {
|
|||||||
{
|
{
|
||||||
id: "msg-voice-locked",
|
id: "msg-voice-locked",
|
||||||
content: "Original voice transcript.",
|
content: "Original voice transcript.",
|
||||||
audioUrl: "https://example.com/voice.mp3",
|
audioUrl: "https://example.com/unlocked-voice.mp3",
|
||||||
locked: false,
|
locked: false,
|
||||||
lockReason: null,
|
lockReason: null,
|
||||||
privateMessageHint: null,
|
privateMessageHint: null,
|
||||||
@@ -802,7 +803,6 @@ describe("chatMachine transitions", () => {
|
|||||||
content: "",
|
content: "",
|
||||||
isFromAI: true,
|
isFromAI: true,
|
||||||
date: "2026-06-29",
|
date: "2026-06-29",
|
||||||
audioUrl: "https://example.com/voice.mp3",
|
|
||||||
locked: true,
|
locked: true,
|
||||||
lockReason: "voice_message",
|
lockReason: "voice_message",
|
||||||
privateMessageHint: "A voice message is waiting.",
|
privateMessageHint: "A voice message is waiting.",
|
||||||
@@ -855,6 +855,14 @@ describe("chatMachine transitions", () => {
|
|||||||
requiredCredits: 10,
|
requiredCredits: 10,
|
||||||
shortfallCredits: 7,
|
shortfallCredits: 7,
|
||||||
});
|
});
|
||||||
|
expect(actor.getSnapshot().context.messages).toMatchObject([
|
||||||
|
{
|
||||||
|
id: "msg-voice-locked",
|
||||||
|
locked: true,
|
||||||
|
lockReason: "voice_message",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(actor.getSnapshot().context.messages[0]?.audioUrl).toBeUndefined();
|
||||||
expect(actor.getSnapshot().context.isUnlockingMessage).toBe(false);
|
expect(actor.getSnapshot().context.isUnlockingMessage).toBe(false);
|
||||||
|
|
||||||
actor.stop();
|
actor.stop();
|
||||||
|
|||||||
@@ -64,7 +64,11 @@ export const unlockMessageActor = fromPromise<
|
|||||||
if (unlockResult.data.unlocked) {
|
if (unlockResult.data.unlocked) {
|
||||||
const markResult = await chatRepo.markPrivateMessageUnlockedInLocal(
|
const markResult = await chatRepo.markPrivateMessageUnlockedInLocal(
|
||||||
input.messageId,
|
input.messageId,
|
||||||
unlockResult.data.lockDetail,
|
{
|
||||||
|
content: unlockResult.data.content,
|
||||||
|
audioUrl: unlockResult.data.audioUrl,
|
||||||
|
lockDetail: unlockResult.data.lockDetail,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
if (Result.isErr(markResult)) {
|
if (Result.isErr(markResult)) {
|
||||||
log.warn("[chat-machine] mark unlocked local message failed", {
|
log.warn("[chat-machine] mark unlocked local message failed", {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export function applySingleUnlockOutput(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...message,
|
...message,
|
||||||
|
audioUrl: getUnlockedAudioUrl(message, output.response),
|
||||||
locked: output.response.lockDetail.locked,
|
locked: output.response.lockDetail.locked,
|
||||||
lockReason: output.response.lockDetail.reason,
|
lockReason: output.response.lockDetail.reason,
|
||||||
imagePaywalled: message.imageUrl
|
imagePaywalled: message.imageUrl
|
||||||
@@ -32,6 +33,15 @@ export function applySingleUnlockOutput(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getUnlockedAudioUrl(
|
||||||
|
message: UiMessage,
|
||||||
|
response: UnlockPrivateResponse,
|
||||||
|
): string | undefined {
|
||||||
|
if (message.lockReason !== "voice_message") return message.audioUrl;
|
||||||
|
if (response.audioUrl.length === 0) return message.audioUrl;
|
||||||
|
return response.audioUrl;
|
||||||
|
}
|
||||||
|
|
||||||
function shouldApplySingleUnlock(message: UiMessage, messageId: string): boolean {
|
function shouldApplySingleUnlock(message: UiMessage, messageId: string): boolean {
|
||||||
if (message.id !== messageId) return false;
|
if (message.id !== messageId) return false;
|
||||||
if (!message.isFromAI) return false;
|
if (!message.isFromAI) return false;
|
||||||
|
|||||||
Reference in New Issue
Block a user