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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user