refactor(chat): remove keyboard compatibility layer

This commit is contained in:
2026-07-16 14:36:33 +08:00
parent aec7cee9cf
commit 8b6bcab05c
9 changed files with 26 additions and 629 deletions
@@ -1,246 +0,0 @@
"use client";
import { useEffect, useRef, type RefObject } from "react";
import { BrowserDetector } from "@/utils/browser-detect";
import { Logger } from "@/utils/logger";
import { PlatformDetector } from "@/utils/platform-detect";
import {
resolveChatKeyboardFallbackLift,
resolveChatKeyboardLift,
type ChatKeyboardLiftResolution,
type ChatKeyboardViewportBaseline,
type ChatKeyboardViewportSnapshot,
} from "./chat-keyboard-geometry";
import { useChatKeyboardDiagnostics } from "./use-chat-keyboard-diagnostics";
const CHAT_INPUT_LIFT_VAR = "--chat-input-lift";
const CHAT_KEYBOARD_FALLBACK_DELAY_MS = 350;
const log = new Logger("UseChatKeyboardAvoidance");
interface VirtualKeyboardLike extends EventTarget {
readonly boundingRect?: Pick<DOMRectReadOnly, "height">;
}
interface NavigatorWithVirtualKeyboard extends Navigator {
readonly virtualKeyboard?: VirtualKeyboardLike;
}
export interface UseChatKeyboardAvoidanceOptions {
active: boolean;
containerRef: RefObject<HTMLElement | null>;
}
export function useChatKeyboardAvoidance({
active,
containerRef,
}: UseChatKeyboardAvoidanceOptions): void {
const activeRef = useRef(active);
const baselineRef = useRef<ChatKeyboardViewportBaseline | null>(null);
useChatKeyboardDiagnostics({ containerRef });
useEffect(() => {
activeRef.current = active;
}, [active]);
useEffect(() => {
if (typeof window === "undefined" || !PlatformDetector.isAndroid()) {
return;
}
const updateBaseline = () => {
if (activeRef.current) return;
baselineRef.current = readViewportBaseline();
};
const visualViewport = window.visualViewport;
updateBaseline();
window.addEventListener("resize", updateBaseline);
window.addEventListener("orientationchange", updateBaseline);
visualViewport?.addEventListener("resize", updateBaseline);
visualViewport?.addEventListener("scroll", updateBaseline);
return () => {
window.removeEventListener("resize", updateBaseline);
window.removeEventListener("orientationchange", updateBaseline);
visualViewport?.removeEventListener("resize", updateBaseline);
visualViewport?.removeEventListener("scroll", updateBaseline);
};
}, []);
useEffect(() => {
const container = containerRef.current;
if (typeof window === "undefined" || !container) return;
if (!active || !PlatformDetector.isAndroid()) {
clearInputLift(container);
if (!active) baselineRef.current = readViewportBaseline();
return;
}
const baseline = baselineRef.current ?? readViewportBaseline();
const visualViewport = window.visualViewport;
const virtualKeyboard = getVirtualKeyboard();
const fallbackAllowed = BrowserDetector.isInAppBrowser();
let frameId: number | null = null;
let fallbackTimerId: number | null = null;
let lastLift = -1;
let pendingCanDismiss = false;
let pendingReason = "initial";
let keyboardObserved = false;
let fallbackApplied = false;
let sessionDismissed = false;
const applyLift = (
lift: number,
source: ChatKeyboardLiftResolution["source"] | "fallback" | "reset",
) => {
const nextLift = Math.max(0, Math.round(lift));
if (nextLift === lastLift) return;
lastLift = nextLift;
container.style.setProperty(CHAT_INPUT_LIFT_VAR, `${nextLift}px`);
log.debug("[chat-keyboard] apply", {
source,
lift: nextLift,
});
};
const clearFallbackTimer = () => {
if (fallbackTimerId == null) return;
window.clearTimeout(fallbackTimerId);
fallbackTimerId = null;
};
const measure = (reason: string, canDismiss: boolean) => {
if (sessionDismissed) return;
const resolution = resolveChatKeyboardLift(
baseline,
readViewportSnapshot(),
);
if (resolution.keyboardVisible) {
keyboardObserved = true;
applyLift(resolution.lift, resolution.source);
return;
}
if ((keyboardObserved || fallbackApplied) && canDismiss) {
sessionDismissed = true;
clearFallbackTimer();
applyLift(0, "reset");
log.debug("[chat-keyboard] dismissed", { reason });
}
};
const scheduleMeasure = (reason: string, canDismiss: boolean) => {
pendingReason = reason;
pendingCanDismiss ||= canDismiss;
if (frameId != null) return;
frameId = window.requestAnimationFrame(() => {
frameId = null;
const nextReason = pendingReason;
const nextCanDismiss = pendingCanDismiss;
pendingCanDismiss = false;
measure(nextReason, nextCanDismiss);
});
};
const handleVisualResize = () => scheduleMeasure("visual-resize", true);
const handleVisualScroll = () => scheduleMeasure("visual-scroll", false);
const handleWindowResize = () => scheduleMeasure("window-resize", true);
const handleVirtualKeyboardChange = () =>
scheduleMeasure("virtual-keyboard", true);
const resetSession = (reason: string) => {
sessionDismissed = true;
clearFallbackTimer();
applyLift(0, "reset");
log.debug("[chat-keyboard] reset", { reason });
};
const handleOrientationChange = () => resetSession("orientationchange");
const handleVisibilityChange = () => {
if (document.visibilityState === "hidden") {
resetSession("visibilitychange");
}
};
visualViewport?.addEventListener("resize", handleVisualResize);
visualViewport?.addEventListener("scroll", handleVisualScroll);
virtualKeyboard?.addEventListener(
"geometrychange",
handleVirtualKeyboardChange,
);
window.addEventListener("resize", handleWindowResize);
window.addEventListener("orientationchange", handleOrientationChange);
document.addEventListener("visibilitychange", handleVisibilityChange);
scheduleMeasure("focus", false);
fallbackTimerId = window.setTimeout(() => {
fallbackTimerId = null;
if (!activeRef.current || sessionDismissed) return;
const resolution = resolveChatKeyboardLift(
baseline,
readViewportSnapshot(),
);
if (resolution.keyboardVisible) {
keyboardObserved = true;
applyLift(resolution.lift, resolution.source);
return;
}
if (!fallbackAllowed) return;
fallbackApplied = true;
applyLift(
resolveChatKeyboardFallbackLift(baseline.layoutHeight),
"fallback",
);
}, CHAT_KEYBOARD_FALLBACK_DELAY_MS);
return () => {
if (frameId != null) window.cancelAnimationFrame(frameId);
clearFallbackTimer();
visualViewport?.removeEventListener("resize", handleVisualResize);
visualViewport?.removeEventListener("scroll", handleVisualScroll);
virtualKeyboard?.removeEventListener(
"geometrychange",
handleVirtualKeyboardChange,
);
window.removeEventListener("resize", handleWindowResize);
window.removeEventListener("orientationchange", handleOrientationChange);
document.removeEventListener("visibilitychange", handleVisibilityChange);
clearInputLift(container);
};
}, [active, containerRef]);
}
function readViewportBaseline(): ChatKeyboardViewportBaseline {
return {
layoutHeight: window.innerHeight,
visualHeight: window.visualViewport?.height ?? window.innerHeight,
};
}
function readViewportSnapshot(): ChatKeyboardViewportSnapshot {
const visualViewport = window.visualViewport;
return {
layoutHeight: window.innerHeight,
visualHeight: visualViewport?.height ?? window.innerHeight,
visualScale: visualViewport?.scale ?? 1,
virtualKeyboardHeight: getVirtualKeyboard()?.boundingRect?.height ?? 0,
};
}
function getVirtualKeyboard(): VirtualKeyboardLike | null {
return (
(navigator as NavigatorWithVirtualKeyboard).virtualKeyboard ?? null
);
}
function clearInputLift(container: HTMLElement): void {
container.style.removeProperty(CHAT_INPUT_LIFT_VAR);
}