147 lines
4.5 KiB
TypeScript
147 lines
4.5 KiB
TypeScript
"use client";
|
|
import type { CommercialAction } from "@/data/schemas/chat";
|
|
|
|
import { CommercialActionCard } from "./commercial-action-card";
|
|
import { ImageBubble } from "./image-bubble";
|
|
import { LockedImageMessageCard } from "./locked-image-message-card";
|
|
import { PrivateMessageCard } from "./private-message-card";
|
|
import { TextBubble } from "./text-bubble";
|
|
import { VoiceBubble } from "./voice-bubble";
|
|
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;
|
|
isFromAI: boolean;
|
|
locked?: boolean | null;
|
|
lockReason?: string | null;
|
|
lockedPrivate?: boolean | null;
|
|
privateMessageHint?: string | null;
|
|
commercialAction?: CommercialAction | null;
|
|
isUnlockingMessage?: boolean;
|
|
onUnlockPrivateMessage?: ChatMessageAction;
|
|
onUnlockVoiceMessage?: ChatMessageAction;
|
|
onUnlockImageMessage?: ChatMessageAction;
|
|
onOpenImage?: (displayMessageId: string) => void;
|
|
onCommercialAction?: (action: CommercialAction) => void;
|
|
onCommercialActionDismiss?: (action: CommercialAction) => void;
|
|
}
|
|
|
|
type ChatMessageAction = (
|
|
displayMessageId: string,
|
|
remoteMessageId?: string,
|
|
) => void;
|
|
|
|
const IMAGE_PLACEHOLDER = "[图片]";
|
|
|
|
export function MessageContent({
|
|
characterId,
|
|
displayMessageId,
|
|
remoteMessageId,
|
|
content,
|
|
imageUrl,
|
|
imagePaywalled,
|
|
audioUrl,
|
|
isFromAI,
|
|
locked,
|
|
lockReason,
|
|
lockedPrivate,
|
|
privateMessageHint,
|
|
commercialAction,
|
|
isUnlockingMessage,
|
|
onUnlockPrivateMessage,
|
|
onUnlockVoiceMessage,
|
|
onUnlockImageMessage,
|
|
onOpenImage,
|
|
onCommercialAction,
|
|
onCommercialActionDismiss,
|
|
}: MessageContentProps) {
|
|
const hasImage = imageUrl != null && imageUrl.length > 0;
|
|
const hasAudio = audioUrl != null && audioUrl.length > 0;
|
|
const hasText = content.length > 0 && content !== IMAGE_PLACEHOLDER;
|
|
const isLockedPrivateMessage =
|
|
lockedPrivate === true ||
|
|
(locked === true && lockReason === "private_message");
|
|
const isLockedVoiceMessage = locked === true && lockReason === "voice_message";
|
|
const isLockedImageMessage =
|
|
locked === true &&
|
|
(lockReason === "image_paywall" || lockReason === "image");
|
|
const shouldRenderVoiceMessage = hasAudio || isLockedVoiceMessage;
|
|
const handleUnlockPrivateMessage =
|
|
onUnlockPrivateMessage
|
|
? () =>
|
|
onUnlockPrivateMessage(displayMessageId, remoteMessageId)
|
|
: undefined;
|
|
const handleUnlockVoiceMessage =
|
|
isLockedVoiceMessage && onUnlockVoiceMessage
|
|
? () => onUnlockVoiceMessage(displayMessageId, remoteMessageId)
|
|
: undefined;
|
|
const handleUnlockImageMessage =
|
|
isLockedImageMessage && onUnlockImageMessage
|
|
? () => onUnlockImageMessage(displayMessageId, remoteMessageId)
|
|
: undefined;
|
|
|
|
return (
|
|
<div
|
|
className={[
|
|
styles.messageContent,
|
|
isFromAI ? styles.messageContentAi : styles.messageContentUser,
|
|
].join(" ")}
|
|
>
|
|
{isLockedPrivateMessage ? (
|
|
<PrivateMessageCard
|
|
hint={privateMessageHint}
|
|
isUnlocking={isUnlockingMessage}
|
|
onUnlock={handleUnlockPrivateMessage}
|
|
/>
|
|
) : isLockedImageMessage && !hasImage ? (
|
|
<LockedImageMessageCard
|
|
hint={privateMessageHint}
|
|
isUnlocking={isUnlockingMessage}
|
|
onUnlock={handleUnlockImageMessage}
|
|
/>
|
|
) : (
|
|
<>
|
|
{hasImage && imageUrl && (
|
|
<ImageBubble
|
|
characterId={characterId}
|
|
displayMessageId={displayMessageId}
|
|
remoteMessageId={remoteMessageId}
|
|
imageUrl={imageUrl}
|
|
imagePaywalled={imagePaywalled}
|
|
onOpenImage={onOpenImage}
|
|
/>
|
|
)}
|
|
{shouldRenderVoiceMessage ? (
|
|
<VoiceBubble
|
|
characterId={characterId}
|
|
remoteMessageId={remoteMessageId}
|
|
audioUrl={audioUrl}
|
|
isFromAI={isFromAI}
|
|
locked={isLockedVoiceMessage}
|
|
hint={privateMessageHint}
|
|
isUnlocking={isUnlockingMessage}
|
|
onUnlock={handleUnlockVoiceMessage}
|
|
/>
|
|
) : null}
|
|
{hasText && !shouldRenderVoiceMessage ? (
|
|
<TextBubble content={content} isFromAI={isFromAI} />
|
|
) : null}
|
|
</>
|
|
)}
|
|
{isFromAI && commercialAction ? (
|
|
<CommercialActionCard
|
|
action={commercialAction}
|
|
onActivate={onCommercialAction}
|
|
onDismiss={onCommercialActionDismiss}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|