revert(dashboard): undo imperative visualViewport write in ChatView

The imperative useLayoutEffect approach made mobile worse — first tap
flickered and didn't bring up the keyboard, while the original
swipe-overlap symptom remained. Restoring the previous React-state
flow until a better fix is identified. Removes the changeset that
shipped with the failed attempt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-06 20:29:12 -07:00
parent 72691c6fd6
commit f08c8ace24
2 changed files with 8 additions and 56 deletions

View File

@@ -1,15 +0,0 @@
---
"@runfusion/fusion": patch
---
Fix ChatView composer sliding over messages on mobile when the user
swipes with the keyboard up. The visualViewport `--vv-height` and
`--vv-offset-top` CSS vars were being routed through React state, so
on iOS — which fires visualViewport scroll/resize on the same frame
as its keyboard animation — the thread translation lagged by one
paint. The composer briefly appeared to slide over the message list
during a pan. The vars are now written imperatively in a
`useLayoutEffect` directly to the `.chat-thread` element on every
visualViewport event, mirroring the working pattern in
`QuickChatFAB.tsx:1032-1052`. Only `--keyboard-overlap` (a structural
open/close signal) still goes through React state.

View File

@@ -1,6 +1,6 @@
// ChatView.css is imported eagerly from App.tsx to avoid a flash of
// unstyled content when the lazy chunk loads. Do not re-import here.
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from "react";
import { memo, useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import type { Components } from "react-markdown";
@@ -793,52 +793,19 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
}
}, []);
const { keyboardOverlap, keyboardOpen } = useMobileKeyboard({
const { keyboardOverlap, viewportHeight, viewportOffsetTop, keyboardOpen } = useMobileKeyboard({
enabled: isMobile && !!activeSession,
});
// Only structural per-open/close vars go through React state. The
// high-frequency --vv-height / --vv-offset-top vars are written
// imperatively below to avoid a one-frame lag during iOS keyboard
// pan that causes the composer to slide over the messages list.
const threadKeyboardStyle: CSSProperties =
keyboardOpen
? ({ "--keyboard-overlap": `${keyboardOverlap}px` } as CSSProperties)
? ({
"--keyboard-overlap": `${keyboardOverlap}px`,
"--vv-offset-top": `${viewportOffsetTop}px`,
...(viewportHeight !== null ? { "--vv-height": `${viewportHeight}px` } : {}),
} as CSSProperties)
: {};
const threadRef = useRef<HTMLDivElement>(null);
// Mirror visualViewport metrics onto the .chat-thread element as CSS
// vars synchronously, bypassing React state. iOS fires
// visualViewport scroll/resize events on the same frame as its own
// keyboard / pan animation; deferring writes through React state
// makes the thread (and its bottom-pinned composer) lag by one paint
// — visible as the composer momentarily sliding over messages while
// the user pans. Mirrors QuickChatFAB.tsx:1032-1052 which uses the
// same approach and works correctly on mobile.
useLayoutEffect(() => {
if (!keyboardOpen) return;
if (typeof window === "undefined" || !window.visualViewport) return;
const thread = threadRef.current;
if (!thread) return;
const vv = window.visualViewport;
const apply = () => {
thread.style.setProperty("--vv-height", `${vv.height}px`);
thread.style.setProperty("--vv-offset-top", `${vv.offsetTop || 0}px`);
};
apply();
vv.addEventListener("resize", apply);
vv.addEventListener("scroll", apply);
return () => {
vv.removeEventListener("resize", apply);
vv.removeEventListener("scroll", apply);
thread.style.removeProperty("--vv-height");
thread.style.removeProperty("--vv-offset-top");
};
}, [keyboardOpen]);
const filteredSkills = useMemo(() => {
const normalizedFilter = skillFilter.trim().toLowerCase();
const matchingSkills = normalizedFilter
@@ -1737,7 +1704,7 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
)}
{/* Thread */}
<div className="chat-thread" ref={threadRef} style={threadKeyboardStyle}>
<div className="chat-thread" style={threadKeyboardStyle}>
{/* Header - always rendered in desktop/tablet, only rendered in mobile when viewing a thread */}
{(hasThreadInView || !isMobile) && (
<div className="chat-thread-header">