feat(FN-2661): add keyboard-aware mobile chat layouts

- Add a shared useMobileKeyboard hook that tracks visualViewport overlap on mobile browsers
- Apply keyboard overlap CSS variables to QuickChatFAB so fullscreen mobile panels resize above the virtual keyboard
- Apply the same keyboard-aware sizing to ChatView mobile thread layout and auto-scroll messages when keyboard opens
- Expand ChatView, QuickChatFAB, and hook test coverage for mobile keyboard detection and viewport-resize behavior
This commit is contained in:
Fusion
2026-04-27 01:22:28 -07:00
committed by gsxdsm
parent fca0fce605
commit 90a28dfe23
8 changed files with 715 additions and 6 deletions

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 { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { 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";
@@ -28,6 +28,7 @@ import { CustomModelDropdown } from "./CustomModelDropdown";
import { AgentMentionPopup } from "./AgentMentionPopup";
import { FileMentionPopup } from "./FileMentionPopup";
import { useFileMention } from "../hooks/useFileMention";
import { useMobileKeyboard } from "../hooks/useMobileKeyboard";
export interface ChatViewProps {
projectId?: string;
@@ -512,6 +513,17 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
const mentionCursorPosRef = useRef(0);
const mode = useViewportMode();
const isMobile = mode === "mobile";
const { keyboardOverlap, viewportHeight } = useMobileKeyboard({
enabled: isMobile && !!activeSession,
});
const threadKeyboardStyle: CSSProperties =
keyboardOverlap > 0
? ({
"--keyboard-overlap": `${keyboardOverlap}px`,
...(viewportHeight !== null ? { "--vv-height": `${viewportHeight}px` } : {}),
} as CSSProperties)
: {};
const filteredSkills = useMemo(() => {
const normalizedFilter = skillFilter.trim().toLowerCase();
@@ -560,6 +572,19 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, streamingText]);
useEffect(() => {
if (keyboardOverlap <= 0) {
return;
}
const messagesContainer = messagesContainerRef.current;
if (!messagesContainer) {
return;
}
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}, [keyboardOverlap]);
// Close context menu on outside click
useEffect(() => {
const handleClick = () => setContextMenu(null);
@@ -1206,7 +1231,7 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
)}
{/* Thread */}
<div className="chat-thread">
<div className="chat-thread" style={threadKeyboardStyle}>
{/* Header - always rendered in desktop/tablet, only rendered in mobile when viewing a thread */}
{(activeSession || !isMobile) && (
<div className="chat-thread-header">