setInstallGuide(null)}
+ role="presentation"
+ >
+
event.stopPropagation()}
+ >
+ setInstallGuide(null)}
+ aria-label="Close install instructions"
+ >
+
+
+
+ {installGuide === "ios" ? (
+
+ ) : (
+
+ )}
+
+ Add CozSweet to Home Screen
+
+ {installGuide === "ios"
+ ? "In Safari, tap the Share button, then choose “Add to Home Screen”."
+ : "Open your browser menu, then choose “Add to Home screen”."}
+
+ setInstallGuide(null)}
+ >
+ Got it
+
+
+
) : null}
);
diff --git a/src/app/chat/chat-screen.tsx b/src/app/chat/chat-screen.tsx
index 8654e3c3..975038fd 100644
--- a/src/app/chat/chat-screen.tsx
+++ b/src/app/chat/chat-screen.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useEffect, useMemo, type CSSProperties } from "react";
+import { useEffect, useMemo, useRef, type CSSProperties } from "react";
import Image from "next/image";
import { useRouter, useSearchParams } from "next/navigation";
@@ -70,6 +70,8 @@ export function ChatScreen() {
const chatDispatch = useChatDispatch();
const authState = useAuthState();
const authDispatch = useAuthDispatch();
+ const imageOpenedFromChatRef = useRef(false);
+ const imageViewerClosingRef = useRef(false);
useSplashLatestMessageSync({
characterId: state.characterId,
historyLoaded: state.historyLoaded,
@@ -127,6 +129,13 @@ export function ChatScreen() {
loginStatus: authState.loginStatus,
});
+ useEffect(() => {
+ if (!imageMessageId) {
+ imageOpenedFromChatRef.current = false;
+ imageViewerClosingRef.current = false;
+ }
+ }, [imageMessageId]);
+
useEffect(() => {
if (
!imageMessageId ||
@@ -218,6 +227,7 @@ export function ChatScreen() {
}
function handleOpenImage(displayMessageId: string): void {
+ imageOpenedFromChatRef.current = true;
router.push(
buildChatImageOverlayUrl(displayMessageId, characterRoutes.chat),
{ scroll: false },
@@ -225,6 +235,13 @@ export function ChatScreen() {
}
function handleCloseImageViewer(): void {
+ if (imageViewerClosingRef.current) return;
+ imageViewerClosingRef.current = true;
+ if (imageOpenedFromChatRef.current) {
+ imageOpenedFromChatRef.current = false;
+ router.back();
+ return;
+ }
router.replace(
buildChatWithoutImageOverlayUrl(searchParams, characterRoutes.chat),
{ scroll: false },
diff --git a/src/app/chat/components/__tests__/fullscreen-image-viewer.test.tsx b/src/app/chat/components/__tests__/fullscreen-image-viewer.test.tsx
new file mode 100644
index 00000000..52db81c0
--- /dev/null
+++ b/src/app/chat/components/__tests__/fullscreen-image-viewer.test.tsx
@@ -0,0 +1,80 @@
+/* @vitest-environment jsdom */
+
+import { act } from "react";
+import { createRoot, type Root } from "react-dom/client";
+import {
+ afterEach,
+ beforeEach,
+ describe,
+ expect,
+ it,
+ type Mock,
+ vi,
+} from "vitest";
+
+import { FullscreenImageViewer } from "../fullscreen-image-viewer";
+
+describe("FullscreenImageViewer", () => {
+ let container: HTMLDivElement;
+ let root: Root;
+ let onClose: Mock<() => void>;
+
+ beforeEach(() => {
+ (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean })
+ .IS_REACT_ACT_ENVIRONMENT = true;
+ container = document.createElement("div");
+ document.body.append(container);
+ root = createRoot(container);
+ onClose = vi.fn<() => void>();
+
+ act(() => {
+ root.render(
+