-
+
{state.authPanelMode === "facebook" ? (
diff --git a/src/app/auth/components/index.ts b/src/app/auth/components/index.ts
index fc76f775..45497f29 100644
--- a/src/app/auth/components/index.ts
+++ b/src/app/auth/components/index.ts
@@ -3,7 +3,6 @@
*/
export * from "./auth-background";
-export * from "./auth-back-button";
export * from "./auth-divider";
export * from "./auth-email-panel";
export * from "./auth-error-message";
diff --git a/src/app/chat/chat-screen.tsx b/src/app/chat/chat-screen.tsx
index 3c32dabf..eee3b8d8 100644
--- a/src/app/chat/chat-screen.tsx
+++ b/src/app/chat/chat-screen.tsx
@@ -55,18 +55,12 @@ export function ChatScreen() {
? "The limit for free chat times\nhas been reached"
: undefined;
const messageLimitCta = isGuest ? "Log in to continue chatting" : undefined;
- const showPhotoPaywallBanner =
- state.paywallTriggered && state.paywallReason === "photo_paywall";
+
const showPrivatePaywallBanner =
state.paywallTriggered && state.paywallReason === "private_paywall";
- const showInlineUpgradeBanner =
- showPhotoPaywallBanner || showPrivatePaywallBanner;
- const inlineUpgradeTitle = showPrivatePaywallBanner
- ? "Unlock VIP to view private messages"
- : "Unlock VIP to view Elio's photos";
- const inlineUpgradeCta = showPrivatePaywallBanner
- ? "Activate VIP to view private messages"
- : "Activate VIP to view photos";
+
+ const inlineUpgradeTitle = "Unlock VIP to view private messages";
+ const inlineUpgradeCta = "Activate VIP to view private messages";
const externalBrowserPromptShownRef = useRef(false);
@@ -143,6 +137,10 @@ export function ChatScreen() {
chatDispatch({ type: "ChatUnlockPrivateMessage", messageId });
}
+ function handleUnlockImagePaywall(): void {
+ router.push(`${ROUTES.subscription}?type=vip`);
+ }
+
function handleMessageLimitUnlock(): void {
if (isGuest) {
router.push(ROUTES.auth);
@@ -175,9 +173,10 @@ export function ChatScreen() {
isGuest={isGuest}
unlockingPrivateMessageId={state.unlockingPrivateMessageId}
onUnlockPrivateMessage={handleUnlockPrivateMessage}
+ onUnlockImagePaywall={handleUnlockImagePaywall}
/>
- {showInlineUpgradeBanner ? (
+ {showPrivatePaywallBanner ? (
void;
+ onUnlockImagePaywall?: () => void;
}
export function ChatArea({
@@ -37,6 +36,7 @@ export function ChatArea({
isReplyingAI,
unlockingPrivateMessageId,
onUnlockPrivateMessage,
+ onUnlockImagePaywall,
}: ChatAreaProps) {
const scrollRef = useRef(null);
const prevLengthRef = useRef(messages.length);
@@ -60,6 +60,7 @@ export function ChatArea({
messages,
unlockingPrivateMessageId,
onUnlockPrivateMessage,
+ onUnlockImagePaywall,
)}
{isReplyingAI && }
@@ -72,6 +73,7 @@ function renderMessagesWithDateHeaders(
messages: readonly UiMessage[],
unlockingPrivateMessageId?: string | null,
onUnlockPrivateMessage?: (messageId: string) => void,
+ onUnlockImagePaywall?: () => void,
) {
const items: Array<{ type: "date"; date: string; key: string } | { type: "msg"; message: UiMessage; key: string }> = [];
@@ -91,6 +93,7 @@ function renderMessagesWithDateHeaders(
id={item.message.id}
content={item.message.content}
imageUrl={item.message.imageUrl}
+ imagePaywalled={item.message.imagePaywalled}
isFromAI={item.message.isFromAI}
privateLocked={item.message.privateLocked}
privateHint={item.message.privateHint}
@@ -99,6 +102,7 @@ function renderMessagesWithDateHeaders(
item.message.id === unlockingPrivateMessageId
}
onUnlockPrivateMessage={onUnlockPrivateMessage}
+ onUnlockImagePaywall={onUnlockImagePaywall}
/>
),
);
diff --git a/src/app/chat/components/fullscreen-image-viewer.module.css b/src/app/chat/components/fullscreen-image-viewer.module.css
index 9219270f..9bd2beaf 100644
--- a/src/app/chat/components/fullscreen-image-viewer.module.css
+++ b/src/app/chat/components/fullscreen-image-viewer.module.css
@@ -11,6 +11,11 @@
cursor: pointer;
}
+.paywallViewer {
+ cursor: default;
+ overflow: hidden;
+}
+
.viewer img {
max-width: 100%;
max-height: 100%;
@@ -19,6 +24,66 @@
-webkit-user-drag: none;
}
+.paywallImage {
+ filter: blur(18px);
+ object-fit: cover;
+ transform: scale(1.08);
+}
+
+.paywallScrim {
+ position: absolute;
+ inset: 0;
+ background:
+ linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0.04) 0%,
+ rgba(255, 255, 255, 0.02) 48%,
+ rgba(255, 255, 255, 0.18) 100%
+ );
+ pointer-events: none;
+}
+
+.backButton {
+ position: absolute;
+ top: 22px;
+ left: 18px;
+ z-index: 1;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 54px;
+ height: 54px;
+ border: 0;
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.94);
+ color: #6d6d6d;
+ box-shadow: 0 6px 14px rgba(0, 0, 0, 0.1);
+ cursor: pointer;
+}
+
+.unlockButton {
+ position: absolute;
+ right: 18px;
+ bottom: max(24px, env(safe-area-inset-bottom));
+ left: 18px;
+ z-index: 1;
+ min-height: 64px;
+ border: 0;
+ border-radius: 999px;
+ background: linear-gradient(90deg, #e05ad7 0%, #ef66b0 100%);
+ box-shadow: 0 8px 16px rgba(130, 60, 90, 0.28);
+ color: #ffffff;
+ cursor: pointer;
+ font-family: var(--font-athelas), Athelas, serif;
+ font-size: 22px;
+ font-weight: 700;
+ letter-spacing: 0.01em;
+}
+
+.unlockButton:active {
+ transform: translateY(1px);
+}
+
.loading {
width: 32px;
height: 32px;
diff --git a/src/app/chat/components/fullscreen-image-viewer.tsx b/src/app/chat/components/fullscreen-image-viewer.tsx
index 1de1a77c..1a6232fd 100644
--- a/src/app/chat/components/fullscreen-image-viewer.tsx
+++ b/src/app/chat/components/fullscreen-image-viewer.tsx
@@ -9,6 +9,7 @@
* - 点击空白 / 下滑手势 → 关闭
* - 双指缩放(CSS `touch-action: pinch-zoom`)
*/
+import { ChevronLeft } from "lucide-react";
import Image from "next/image";
import { useEffect } from "react";
@@ -16,10 +17,17 @@ import styles from "./fullscreen-image-viewer.module.css";
export interface FullscreenImageViewerProps {
imageUrl: string;
+ imagePaywalled?: boolean;
+ onUnlockImagePaywall?: () => void;
onClose: () => void;
}
-export function FullscreenImageViewer({ imageUrl, onClose }: FullscreenImageViewerProps) {
+export function FullscreenImageViewer({
+ imageUrl,
+ imagePaywalled = false,
+ onUnlockImagePaywall,
+ onClose,
+}: FullscreenImageViewerProps) {
useEffect(() => {
const handleKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
@@ -28,6 +36,44 @@ export function FullscreenImageViewer({ imageUrl, onClose }: FullscreenImageView
return () => document.removeEventListener("keydown", handleKey);
}, [onClose]);
+ const src = imageUrl.startsWith("data:") || imageUrl.startsWith("http")
+ ? imageUrl
+ : `data:image/png;base64,${imageUrl}`;
+
+ if (imagePaywalled) {
+ return (
+
+
+
+
+
+
+ );
+ }
+
return (
void;
}
-export function ImageBubble({ imageUrl }: ImageBubbleProps) {
+export function ImageBubble({
+ imageUrl,
+ imagePaywalled = false,
+ onUnlockImagePaywall,
+}: ImageBubbleProps) {
const [open, setOpen] = useState(false);
const [error, setError] = useState(false);
@@ -53,7 +57,14 @@ export function ImageBubble({ imageUrl }: ImageBubbleProps) {
/>
)}
- {open && setOpen(false)} />}
+ {open && (
+ setOpen(false)}
+ />
+ )}
>
);
}
diff --git a/src/app/chat/components/message-bubble.tsx b/src/app/chat/components/message-bubble.tsx
index 3c6c10e7..7940543b 100644
--- a/src/app/chat/components/message-bubble.tsx
+++ b/src/app/chat/components/message-bubble.tsx
@@ -18,22 +18,26 @@ export interface MessageBubbleProps {
id?: string;
content: string;
imageUrl?: string | null;
+ imagePaywalled?: boolean;
isFromAI: boolean;
privateLocked?: boolean | null;
privateHint?: string | null;
isUnlockingPrivate?: boolean;
onUnlockPrivateMessage?: (messageId: string) => void;
+ onUnlockImagePaywall?: () => void;
}
export function MessageBubble({
id,
content,
imageUrl,
+ imagePaywalled,
isFromAI,
privateLocked,
privateHint,
isUnlockingPrivate,
onUnlockPrivateMessage,
+ onUnlockImagePaywall,
}: MessageBubbleProps) {
const { avatarUrl } = useUserState();
@@ -46,11 +50,13 @@ export function MessageBubble({
messageId={id}
content={content}
imageUrl={imageUrl}
+ imagePaywalled={imagePaywalled}
isFromAI={true}
privateLocked={privateLocked}
privateHint={privateHint}
isUnlockingPrivate={isUnlockingPrivate}
onUnlockPrivateMessage={onUnlockPrivateMessage}
+ onUnlockImagePaywall={onUnlockImagePaywall}
/>
{/* 占位:保持与头像宽度一致 */}
@@ -64,11 +70,13 @@ export function MessageBubble({
messageId={id}
content={content}
imageUrl={imageUrl}
+ imagePaywalled={imagePaywalled}
isFromAI={false}
privateLocked={privateLocked}
privateHint={privateHint}
isUnlockingPrivate={isUnlockingPrivate}
onUnlockPrivateMessage={onUnlockPrivateMessage}
+ onUnlockImagePaywall={onUnlockImagePaywall}
/>