refactor(characters): drive shared UI from active character
This commit is contained in:
@@ -48,7 +48,13 @@ describe("shared Tailwind components", () => {
|
||||
|
||||
it("renders CharacterAvatar with dynamic sizing and image utilities", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<CharacterAvatar size={48} imageSize={96} className="custom-avatar" />,
|
||||
<CharacterAvatar
|
||||
src="/images/avatar/maya.png"
|
||||
alt="Maya Tan"
|
||||
size={48}
|
||||
imageSize={96}
|
||||
className="custom-avatar"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(html).toContain("inline-flex");
|
||||
@@ -56,7 +62,7 @@ describe("shared Tailwind components", () => {
|
||||
expect(html).toContain("width:48px");
|
||||
expect(html).toContain("height:48px");
|
||||
expect(html).toContain("size-full object-cover");
|
||||
expect(html).toContain("%2Fimages%2Favatar%2Felio.png");
|
||||
expect(html).toContain("%2Fimages%2Favatar%2Fmaya.png");
|
||||
});
|
||||
|
||||
it("renders UserMessageAvatar custom and fallback images", () => {
|
||||
|
||||
@@ -4,8 +4,8 @@ import Image from "next/image";
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
export interface CharacterAvatarProps {
|
||||
src?: string;
|
||||
alt?: string;
|
||||
src: string;
|
||||
alt: string;
|
||||
className?: string;
|
||||
size?: number | string;
|
||||
imageSize?: number;
|
||||
@@ -13,8 +13,8 @@ export interface CharacterAvatarProps {
|
||||
}
|
||||
|
||||
export function CharacterAvatar({
|
||||
src = "/images/avatar/elio.png",
|
||||
alt = "Elio Silvestri",
|
||||
src,
|
||||
alt,
|
||||
className,
|
||||
size = 43,
|
||||
imageSize,
|
||||
|
||||
@@ -79,6 +79,7 @@ describe("core Tailwind components", () => {
|
||||
<AppBottomNav
|
||||
activeItem="chat"
|
||||
variant="dark"
|
||||
privateRoomLabel="Maya Private room"
|
||||
onChatClick={() => undefined}
|
||||
onPrivateRoomClick={() => undefined}
|
||||
/>,
|
||||
@@ -87,6 +88,7 @@ describe("core Tailwind components", () => {
|
||||
<AppBottomNav
|
||||
activeItem="privateRoom"
|
||||
variant="warm"
|
||||
privateRoomLabel="Maya Private room"
|
||||
onChatClick={() => undefined}
|
||||
onPrivateRoomClick={() => undefined}
|
||||
/>,
|
||||
@@ -99,7 +101,7 @@ describe("core Tailwind components", () => {
|
||||
);
|
||||
expect(privateRoomHtml).toMatch(/class="[^"]*warm[^"]*"/);
|
||||
expect(privateRoomHtml).toMatch(
|
||||
/<button[^>]*aria-current="page"[^>]*>.*?<span>Elio Private room<\/span>/,
|
||||
/<button[^>]*aria-current="page"[^>]*>.*?<span>Maya Private room<\/span>/,
|
||||
);
|
||||
expect(chatHtml).toContain('data-analytics-key="navigation.chat"');
|
||||
expect(privateRoomHtml).toContain(
|
||||
|
||||
@@ -10,7 +10,7 @@ export type AppBottomNavVariant = "warm" | "dark";
|
||||
export interface AppBottomNavProps {
|
||||
activeItem?: AppBottomNavItem | null;
|
||||
variant?: AppBottomNavVariant;
|
||||
privateRoomLabel?: string;
|
||||
privateRoomLabel: string;
|
||||
onChatClick: () => void;
|
||||
onPrivateRoomClick: () => void;
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export interface AppBottomNavProps {
|
||||
export function AppBottomNav({
|
||||
activeItem = null,
|
||||
variant = "warm",
|
||||
privateRoomLabel = "Elio Private room",
|
||||
privateRoomLabel,
|
||||
onChatClick,
|
||||
onPrivateRoomClick,
|
||||
}: AppBottomNavProps) {
|
||||
|
||||
@@ -16,7 +16,10 @@ export default async function CharacterChatLayout({
|
||||
if (!character?.capabilities.chat) notFound();
|
||||
|
||||
return (
|
||||
<ChatRouteProviders characterId={character.id}>
|
||||
<ChatRouteProviders
|
||||
characterId={character.id}
|
||||
emptyChatGreeting={character.emptyChatGreeting}
|
||||
>
|
||||
{children}
|
||||
</ChatRouteProviders>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
useChatDispatch,
|
||||
useChatState,
|
||||
} from "@/stores/chat/chat-context";
|
||||
import { useActiveCharacterRoutes } from "@/providers/character-provider";
|
||||
import {
|
||||
useActiveCharacter,
|
||||
useActiveCharacterRoutes,
|
||||
} from "@/providers/character-provider";
|
||||
|
||||
import { MobileShell } from "@/app/_components/core";
|
||||
|
||||
@@ -46,6 +49,7 @@ const chatShellStyle = {
|
||||
|
||||
export function ChatScreen() {
|
||||
const router = useRouter();
|
||||
const character = useActiveCharacter();
|
||||
const characterRoutes = useActiveCharacterRoutes();
|
||||
const searchParams = useSearchParams();
|
||||
const state = useChatState();
|
||||
@@ -167,7 +171,7 @@ export function ChatScreen() {
|
||||
>
|
||||
<div className="pointer-events-none absolute inset-0 z-0">
|
||||
<Image
|
||||
src="/images/chat/bg-chatpage.png"
|
||||
src={character.assets.chatBackground}
|
||||
alt=""
|
||||
fill
|
||||
priority
|
||||
|
||||
@@ -235,6 +235,7 @@ function renderChatArea(
|
||||
act(() => {
|
||||
root.render(
|
||||
<ChatArea
|
||||
characterId="character_elio"
|
||||
messages={messages}
|
||||
isReplyingAI={false}
|
||||
initialScrollReady={initialScrollReady}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getCharacterBySlug } from "@/data/constants/character";
|
||||
import { CharacterProvider } from "@/providers/character-provider";
|
||||
|
||||
import { LockedImageMessageCard } from "../locked-image-message-card";
|
||||
|
||||
describe("LockedImageMessageCard", () => {
|
||||
it("renders the promotion hint and an enabled unlock action", () => {
|
||||
const character = getCharacterBySlug("maya");
|
||||
if (!character) throw new Error("Missing Maya character fixture");
|
||||
const html = renderToStaticMarkup(
|
||||
<LockedImageMessageCard
|
||||
hint="Unlock this private photo."
|
||||
onUnlock={() => undefined}
|
||||
/>,
|
||||
<CharacterProvider character={character}>
|
||||
<LockedImageMessageCard
|
||||
hint="Unlock this private photo."
|
||||
onUnlock={() => undefined}
|
||||
/>
|
||||
</CharacterProvider>,
|
||||
);
|
||||
|
||||
expect(html).toContain('aria-label="Locked private image"');
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
@@ -25,8 +26,8 @@ vi.mock("@/router/use-app-navigator", () => ({
|
||||
|
||||
describe("chat Tailwind components", () => {
|
||||
it("renders MessageAvatar AI and user branches with image utilities", () => {
|
||||
const aiHtml = renderToStaticMarkup(<MessageAvatar isFromAI={true} />);
|
||||
const userHtml = renderToStaticMarkup(
|
||||
const aiHtml = renderWithCharacter(<MessageAvatar isFromAI={true} />);
|
||||
const userHtml = renderWithCharacter(
|
||||
<MessageAvatar isFromAI={false} userAvatarUrl="/user-avatar.png" />,
|
||||
);
|
||||
|
||||
@@ -71,8 +72,8 @@ describe("chat Tailwind components", () => {
|
||||
});
|
||||
|
||||
it("renders PrivateMessageCard with fallback and loading states", () => {
|
||||
const fallbackHtml = renderToStaticMarkup(<PrivateMessageCard />);
|
||||
const unlockingHtml = renderToStaticMarkup(
|
||||
const fallbackHtml = renderWithCharacter(<PrivateMessageCard />);
|
||||
const unlockingHtml = renderWithCharacter(
|
||||
<PrivateMessageCard hint="Premium secret" isUnlocking onUnlock={() => undefined} />,
|
||||
);
|
||||
|
||||
@@ -126,13 +127,18 @@ describe("chat Tailwind components", () => {
|
||||
it("renders ImageBubble openable and paywalled states", () => {
|
||||
const openableHtml = renderToStaticMarkup(
|
||||
<ImageBubble
|
||||
characterId="character_elio"
|
||||
messageId="message-1"
|
||||
imageUrl="/chat-image.png"
|
||||
onOpenImage={() => undefined}
|
||||
/>,
|
||||
);
|
||||
const paywalledHtml = renderToStaticMarkup(
|
||||
<ImageBubble imageUrl="/locked-image.png" imagePaywalled />,
|
||||
<ImageBubble
|
||||
characterId="character_elio"
|
||||
imageUrl="/locked-image.png"
|
||||
imagePaywalled
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(openableHtml).toContain('role="button"');
|
||||
@@ -170,14 +176,14 @@ describe("chat Tailwind components", () => {
|
||||
});
|
||||
|
||||
it("renders ChatHeader guest and member branches", () => {
|
||||
const guestHtml = renderToStaticMarkup(<ChatHeader isGuest={true} />);
|
||||
const memberHtml = renderToStaticMarkup(
|
||||
const guestHtml = renderWithCharacter(<ChatHeader isGuest={true} />);
|
||||
const memberHtml = renderWithCharacter(
|
||||
<ChatHeader isGuest={false} offerBanner={<div>Offer</div>} />,
|
||||
);
|
||||
const memberWithHintHtml = renderToStaticMarkup(
|
||||
const memberWithHintHtml = renderWithCharacter(
|
||||
<ChatHeader isGuest={false} showBrowserHint />,
|
||||
);
|
||||
const guestWithHintHtml = renderToStaticMarkup(
|
||||
const guestWithHintHtml = renderWithCharacter(
|
||||
<ChatHeader isGuest={true} showBrowserHint />,
|
||||
);
|
||||
|
||||
@@ -369,3 +375,11 @@ describe("chat Tailwind components", () => {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function renderWithCharacter(node: ReactNode, slug = "elio"): string {
|
||||
const character = getCharacterBySlug(slug);
|
||||
if (!character) throw new Error(`Missing ${slug} character fixture`);
|
||||
return renderToStaticMarkup(
|
||||
<CharacterProvider character={character}>{node}</CharacterProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
|
||||
import type { UiMessage } from "@/stores/chat/ui-message";
|
||||
import { DEFAULT_CHARACTER_ID } from "@/data/constants/character";
|
||||
import { usePullToRefresh } from "@/hooks/use-pull-to-refresh";
|
||||
|
||||
import {
|
||||
@@ -46,7 +45,7 @@ const ReplyingAnimation = lazy(() =>
|
||||
);
|
||||
|
||||
export interface ChatAreaProps {
|
||||
characterId?: string;
|
||||
characterId: string;
|
||||
messages: readonly UiMessage[];
|
||||
isReplyingAI: boolean;
|
||||
scrollToBottomSignal?: number;
|
||||
@@ -63,7 +62,7 @@ export interface ChatAreaProps {
|
||||
}
|
||||
|
||||
export function ChatArea({
|
||||
characterId = DEFAULT_CHARACTER_ID,
|
||||
characterId,
|
||||
messages,
|
||||
isReplyingAI,
|
||||
scrollToBottomSignal = 0,
|
||||
|
||||
@@ -9,10 +9,9 @@
|
||||
*/
|
||||
|
||||
import { ChatMediaImage } from "./chat-media-image";
|
||||
import { DEFAULT_CHARACTER_ID } from "@/data/constants/character";
|
||||
|
||||
export interface ImageBubbleProps {
|
||||
characterId?: string;
|
||||
characterId: string;
|
||||
messageId?: string;
|
||||
imageUrl: string; // base64 data URI 或 URL
|
||||
imagePaywalled?: boolean;
|
||||
@@ -20,7 +19,7 @@ export interface ImageBubbleProps {
|
||||
}
|
||||
|
||||
export function ImageBubble({
|
||||
characterId = DEFAULT_CHARACTER_ID,
|
||||
characterId,
|
||||
messageId,
|
||||
imageUrl,
|
||||
imagePaywalled = false,
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
import { CoinsRulesScreen } from "./coins-rules-screen";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function CoinsRulesPage() {
|
||||
return <CoinsRulesScreen />;
|
||||
import { DEFAULT_CHARACTER_SLUG } from "@/data/constants/character";
|
||||
import {
|
||||
appendRouteSearchParams,
|
||||
getCharacterRoutes,
|
||||
type RouteSearchParams,
|
||||
} from "@/router/routes";
|
||||
|
||||
export default async function CoinsRulesPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<RouteSearchParams>;
|
||||
}) {
|
||||
redirect(
|
||||
appendRouteSearchParams(
|
||||
getCharacterRoutes(DEFAULT_CHARACTER_SLUG).coinsRules,
|
||||
await searchParams,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { getCharacterBySlug } from "@/data/constants/character";
|
||||
import { CharacterProvider } from "@/providers/character-provider";
|
||||
import { Result } from "@/utils/result";
|
||||
|
||||
import { FeedbackScreen } from "../feedback-screen";
|
||||
@@ -48,7 +50,7 @@ describe("FeedbackScreen", () => {
|
||||
});
|
||||
|
||||
it("renders all categories and defaults to Problem", () => {
|
||||
act(() => root.render(<FeedbackScreen />));
|
||||
renderFeedbackScreen(root);
|
||||
|
||||
expect(container.textContent).toContain("Help us improve CozSweet");
|
||||
expect(container.textContent).toContain("Problem");
|
||||
@@ -73,7 +75,7 @@ describe("FeedbackScreen", () => {
|
||||
mocks.submitFeedback.mockResolvedValue(
|
||||
Result.ok({ feedbackId: "feedback-123" }),
|
||||
);
|
||||
act(() => root.render(<FeedbackScreen />));
|
||||
renderFeedbackScreen(root);
|
||||
const textarea = container.querySelector("textarea");
|
||||
if (!textarea) throw new Error("Expected feedback textarea");
|
||||
|
||||
@@ -104,6 +106,18 @@ describe("FeedbackScreen", () => {
|
||||
});
|
||||
});
|
||||
|
||||
function renderFeedbackScreen(root: Root): void {
|
||||
const character = getCharacterBySlug("maya");
|
||||
if (!character) throw new Error("Missing Maya character fixture");
|
||||
act(() =>
|
||||
root.render(
|
||||
<CharacterProvider character={character}>
|
||||
<FeedbackScreen />
|
||||
</CharacterProvider>,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function setTextareaValue(element: HTMLTextAreaElement, value: string): void {
|
||||
const setter = Object.getOwnPropertyDescriptor(
|
||||
HTMLTextAreaElement.prototype,
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
import { FeedbackScreen } from "./feedback-screen";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function FeedbackPage() {
|
||||
return <FeedbackScreen />;
|
||||
import { DEFAULT_CHARACTER_SLUG } from "@/data/constants/character";
|
||||
import {
|
||||
appendRouteSearchParams,
|
||||
getCharacterRoutes,
|
||||
type RouteSearchParams,
|
||||
} from "@/router/routes";
|
||||
|
||||
export default async function FeedbackPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<RouteSearchParams>;
|
||||
}) {
|
||||
redirect(
|
||||
appendRouteSearchParams(
|
||||
getCharacterRoutes(DEFAULT_CHARACTER_SLUG).feedback,
|
||||
await searchParams,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ import { act, type Dispatch } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { getCharacterBySlug } from "@/data/constants/character";
|
||||
import type { LoginStatus } from "@/data/schemas/auth";
|
||||
import { CharacterProvider } from "@/providers/character-provider";
|
||||
import { getCharacterRoutes } from "@/router/routes";
|
||||
import type { PrivateRoomEvent } from "@/stores/private-room";
|
||||
import type { PrivateRoomUnlockPaywallRequest } from "@/stores/private-room/private-room-state";
|
||||
@@ -80,7 +82,7 @@ describe("Private Room paywall navigation", () => {
|
||||
renderHarness(root, "guest", roomDispatch);
|
||||
|
||||
expect(mocks.openAuth).toHaveBeenCalledWith(
|
||||
getCharacterRoutes("elio").privateRoom,
|
||||
getCharacterRoutes("maya").privateRoom,
|
||||
);
|
||||
expect(mocks.openSubscription).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -91,9 +93,13 @@ function renderHarness(
|
||||
loginStatus: LoginStatus,
|
||||
roomDispatch: Dispatch<PrivateRoomEvent>,
|
||||
): void {
|
||||
const character = getCharacterBySlug("maya");
|
||||
if (!character) throw new Error("Missing Maya character fixture");
|
||||
act(() => {
|
||||
root.render(
|
||||
<Harness loginStatus={loginStatus} roomDispatch={roomDispatch} />,
|
||||
<CharacterProvider character={character}>
|
||||
<Harness loginStatus={loginStatus} roomDispatch={roomDispatch} />
|
||||
</CharacterProvider>,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
import { SidebarScreen } from "@/app/sidebar/sidebar-screen";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function SidebarPage() {
|
||||
return <SidebarScreen />;
|
||||
import { DEFAULT_CHARACTER_SLUG } from "@/data/constants/character";
|
||||
import {
|
||||
appendRouteSearchParams,
|
||||
getCharacterRoutes,
|
||||
type RouteSearchParams,
|
||||
} from "@/router/routes";
|
||||
|
||||
export default async function SidebarPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<RouteSearchParams>;
|
||||
}) {
|
||||
redirect(
|
||||
appendRouteSearchParams(
|
||||
getCharacterRoutes(DEFAULT_CHARACTER_SLUG).sidebar,
|
||||
await searchParams,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,21 +17,23 @@ describe("splash Tailwind components", () => {
|
||||
});
|
||||
|
||||
it("renders SplashBackground with Tailwind fill image classes", () => {
|
||||
const html = renderToStaticMarkup(<SplashBackground />);
|
||||
const html = renderToStaticMarkup(
|
||||
<SplashBackground src="/images/cover/maya.png" />,
|
||||
);
|
||||
|
||||
expect(html).toContain("absolute");
|
||||
expect(html).toContain("bg-sidebar-background");
|
||||
expect(html).toContain("object-cover");
|
||||
expect(html).toContain("%2Fimages%2Fcover%2Felio.png");
|
||||
expect(html).toContain("%2Fimages%2Fcover%2Fmaya.png");
|
||||
});
|
||||
|
||||
it("renders SplashContent with Tailwind typography classes", () => {
|
||||
const html = renderToStaticMarkup(<SplashContent />);
|
||||
const html = renderToStaticMarkup(<SplashContent characterName="Maya" />);
|
||||
|
||||
expect(html).toContain("flex flex-col gap-md");
|
||||
expect(html).toContain("font-(family-name:--font-athelas)");
|
||||
expect(html).toContain("whitespace-pre-line");
|
||||
expect(html).toContain("Welcome to my secret hideout");
|
||||
expect(html).toContain("Welcome to Maya");
|
||||
});
|
||||
|
||||
it("renders SplashButton as an always-ready action", () => {
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
import Image from "next/image";
|
||||
|
||||
export function SplashBackground({
|
||||
src = "/images/cover/elio.png",
|
||||
src,
|
||||
}: {
|
||||
src?: string;
|
||||
src: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="absolute inset-0 z-0 overflow-hidden bg-sidebar-background">
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
|
||||
const APOSTROPHE = "'";
|
||||
|
||||
export function SplashContent() {
|
||||
export function SplashContent({ characterName }: { characterName: string }) {
|
||||
return (
|
||||
<div className="z-2 flex flex-col gap-md">
|
||||
<h1 className="m-0 whitespace-pre-line text-left font-(family-name:--font-athelas) text-[clamp(28px,7.8vw,30px)] font-bold italic leading-[1.28] text-white">
|
||||
Welcome to my secret hideout~
|
||||
Welcome to {characterName}{APOSTROPHE}s secret hideout~
|
||||
{"\n"}It{APOSTROPHE}s just the two of us now.
|
||||
{"\n"}Feel free to whisper your
|
||||
{"\n"}little secrets.
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useSplashLatestMessage } from "../use-splash-latest-message";
|
||||
|
||||
function LatestMessageHarness() {
|
||||
const state = useSplashLatestMessage({
|
||||
characterId: "character_maya",
|
||||
hasInitialized: true,
|
||||
isAuthLoading: false,
|
||||
loginStatus: "facebook",
|
||||
@@ -64,5 +65,9 @@ describe("useSplashLatestMessage hydration", () => {
|
||||
expect(hydrationErrors).toEqual([]);
|
||||
expect(container.textContent).toBe("Loading");
|
||||
expect(mocks.loadLatestMessage).toHaveBeenCalledOnce();
|
||||
expect(mocks.loadLatestMessage).toHaveBeenCalledWith({
|
||||
cache: mocks.cache,
|
||||
characterId: "character_maya",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import type { LoginStatus } from "@/data/schemas/auth";
|
||||
import { DEFAULT_CHARACTER_ID } from "@/data/constants/character";
|
||||
import { useHasHydrated } from "@/hooks/use-has-hydrated";
|
||||
import { loadSplashLatestMessagePreview } from "@/lib/chat/splash_latest_message";
|
||||
import { useSplashLatestMessageCache } from "@/providers/splash-latest-message-provider";
|
||||
@@ -13,7 +12,7 @@ import { Result } from "@/utils/result";
|
||||
const log = new Logger("SplashLatestMessage");
|
||||
|
||||
export interface UseSplashLatestMessageInput {
|
||||
characterId?: string;
|
||||
characterId: string;
|
||||
hasInitialized: boolean;
|
||||
isAuthLoading: boolean;
|
||||
loginStatus: LoginStatus;
|
||||
@@ -31,7 +30,7 @@ interface SplashLatestMessageState {
|
||||
}
|
||||
|
||||
export function useSplashLatestMessage({
|
||||
characterId = DEFAULT_CHARACTER_ID,
|
||||
characterId,
|
||||
hasInitialized,
|
||||
isAuthLoading,
|
||||
loginStatus,
|
||||
|
||||
@@ -65,12 +65,12 @@ export function SplashScreen() {
|
||||
isLoading={latestMessage.isLoading}
|
||||
onOpenChat={handleStartChat}
|
||||
/>
|
||||
<SplashContent />
|
||||
<SplashContent characterName={character.shortName} />
|
||||
<div className={styles.buttonArea}>
|
||||
<SplashButton onStartChat={handleStartChat} />
|
||||
</div>
|
||||
<p className={styles.bottom}>
|
||||
{character.displayName}, {character.copy.splashRelationship}
|
||||
{character.displayName}, {character.tagline}
|
||||
<br />
|
||||
24/7 online | Chat | Companion | Heal | Sweet moments
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user