feat(chat): sync multi-role backend APIs
This commit is contained in:
@@ -5,7 +5,7 @@ import {
|
||||
CHARACTERS,
|
||||
getCharacterBySlug,
|
||||
} from "@/data/constants/character";
|
||||
import { CharacterProvider } from "@/providers/character-provider";
|
||||
import { CharacterRouteBoundary } from "@/providers/character-route-boundary";
|
||||
|
||||
export const dynamicParams = false;
|
||||
|
||||
@@ -26,5 +26,9 @@ export default async function CharacterLayout({
|
||||
const character = getCharacterBySlug(characterSlug);
|
||||
if (!character) notFound();
|
||||
|
||||
return <CharacterProvider character={character}>{children}</CharacterProvider>;
|
||||
return (
|
||||
<CharacterRouteBoundary character={character}>
|
||||
{children}
|
||||
</CharacterRouteBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const defaultScope = {
|
||||
};
|
||||
|
||||
const promotionSession: PendingChatPromotion = {
|
||||
characterId: "character_elio",
|
||||
characterId: "elio",
|
||||
promotionType: "image",
|
||||
lockType: "image_paywall",
|
||||
clientLockId: "promotion-1",
|
||||
@@ -182,7 +182,7 @@ function createPendingUnlock(
|
||||
): PendingChatUnlock {
|
||||
return {
|
||||
reason: "single_message_unlock",
|
||||
characterId: "character_elio",
|
||||
characterId: "elio",
|
||||
...input,
|
||||
stage: "auth",
|
||||
createdAt: 1,
|
||||
|
||||
@@ -13,6 +13,9 @@ import {
|
||||
useActiveCharacter,
|
||||
useActiveCharacterRoutes,
|
||||
} from "@/providers/character-provider";
|
||||
import { useCharacterCatalog } from "@/providers/character-catalog-provider";
|
||||
import { clearPendingChatNavigation } from "@/lib/navigation/chat_unlock_session";
|
||||
import { getCharacterRoutes } from "@/router/routes";
|
||||
|
||||
import { MobileShell } from "@/app/_components/core";
|
||||
|
||||
@@ -50,6 +53,9 @@ const chatShellStyle = {
|
||||
export function ChatScreen() {
|
||||
const router = useRouter();
|
||||
const character = useActiveCharacter();
|
||||
const characterCatalog = useCharacterCatalog();
|
||||
const refreshCharacterCatalog = characterCatalog.refresh;
|
||||
const defaultCharacterSlug = characterCatalog.defaultCharacter.slug;
|
||||
const characterRoutes = useActiveCharacterRoutes();
|
||||
const searchParams = useSearchParams();
|
||||
const state = useChatState();
|
||||
@@ -133,6 +139,42 @@ export function ChatScreen() {
|
||||
state.historyLoaded,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const errorCode = state.characterErrorCode;
|
||||
if (errorCode === "CHARACTER_MISMATCH") {
|
||||
chatDispatch({ type: "ChatHistoryRefreshRequested" });
|
||||
return;
|
||||
}
|
||||
if (
|
||||
errorCode !== "CHARACTER_DISABLED" &&
|
||||
errorCode !== "CHARACTER_NOT_FOUND"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
if (errorCode === "CHARACTER_NOT_FOUND") {
|
||||
await clearPendingChatNavigation();
|
||||
}
|
||||
await refreshCharacterCatalog();
|
||||
if (!cancelled) {
|
||||
router.replace(
|
||||
getCharacterRoutes(defaultCharacterSlug).splash,
|
||||
);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [
|
||||
chatDispatch,
|
||||
defaultCharacterSlug,
|
||||
refreshCharacterCatalog,
|
||||
router,
|
||||
state.characterErrorCode,
|
||||
]);
|
||||
|
||||
function handleUnlockPrivateMessage(messageId: string): void {
|
||||
unlockCoordinator.requestMessageUnlock(messageId, "private");
|
||||
}
|
||||
@@ -223,7 +265,9 @@ export function ChatScreen() {
|
||||
onUnlock={messageLimitBanner.unlock}
|
||||
/>
|
||||
) : (
|
||||
<ChatInputBar disabled={!state.historyLoaded} />
|
||||
<ChatInputBar
|
||||
disabled={!state.historyLoaded || !state.canSendMessage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -235,7 +235,7 @@ function renderChatArea(
|
||||
act(() => {
|
||||
root.render(
|
||||
<ChatArea
|
||||
characterId="character_elio"
|
||||
characterId="elio"
|
||||
messages={messages}
|
||||
isReplyingAI={false}
|
||||
initialScrollReady={initialScrollReady}
|
||||
|
||||
@@ -127,7 +127,7 @@ describe("chat Tailwind components", () => {
|
||||
it("renders ImageBubble openable and paywalled states", () => {
|
||||
const openableHtml = renderToStaticMarkup(
|
||||
<ImageBubble
|
||||
characterId="character_elio"
|
||||
characterId="elio"
|
||||
messageId="message-1"
|
||||
imageUrl="/chat-image.png"
|
||||
onOpenImage={() => undefined}
|
||||
@@ -135,7 +135,7 @@ describe("chat Tailwind components", () => {
|
||||
);
|
||||
const paywalledHtml = renderToStaticMarkup(
|
||||
<ImageBubble
|
||||
characterId="character_elio"
|
||||
characterId="elio"
|
||||
imageUrl="/locked-image.png"
|
||||
imagePaywalled
|
||||
/>,
|
||||
|
||||
@@ -73,6 +73,7 @@ export const ChatInputTextField = forwardRef<
|
||||
onBlur={() => onFocusChange?.(false)}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
maxLength={4000}
|
||||
autoFocus={autoFocus}
|
||||
rows={1}
|
||||
aria-label="Message"
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export default function LegacyChatLayout({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export default function LegacyPrivateRoomLayout({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return children;
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import { useSplashLatestMessage } from "../use-splash-latest-message";
|
||||
|
||||
function LatestMessageHarness() {
|
||||
const state = useSplashLatestMessage({
|
||||
characterId: "character_maya",
|
||||
characterId: "maya-tan",
|
||||
hasInitialized: true,
|
||||
isAuthLoading: false,
|
||||
loginStatus: "facebook",
|
||||
@@ -67,7 +67,7 @@ describe("useSplashLatestMessage hydration", () => {
|
||||
expect(mocks.loadLatestMessage).toHaveBeenCalledOnce();
|
||||
expect(mocks.loadLatestMessage).toHaveBeenCalledWith({
|
||||
cache: mocks.cache,
|
||||
characterId: "character_maya",
|
||||
characterId: "maya-tan",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export default function LegacyTipLayout({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return children;
|
||||
}
|
||||
Reference in New Issue
Block a user