"use client"; /** * 软键盘避让 hook。 * * 目标环境:小米系 Android 设备上的 Facebook IAB。 * * 策略: * 1. 持续写入 --app-viewport-height,聊天页高度不再只依赖 100dvh。 * 2. 优先使用 visualViewport 计算键盘高度。 * 3. 输入框聚焦后测量目标元素是否越过可见底部,计算额外抬升。 * 4. 小米系 Android + Facebook IAB 若 viewport 未可靠变化,启用保守 fallback 抬升; * 键盘收起后若焦点仍停留在 textarea,也要主动释放 fallback。 */ import { useEffect, useRef } from "react"; import { Logger } from "@/utils"; import { RECHECK_DELAYS_MS, RECHECK_EVENT, } from "./constants"; import { resetKeyboardVars, setInputLiftVar, setKeyboardHeightVar, setViewportHeightVar, } from "./css-vars"; import { getKeyboardAdaptEnvironment, shouldEnableKeyboardAdaptation, } from "./environment"; import { blurKeyboardTarget, isKeyboardTargetActive, } from "./keyboard-target"; import { addKeyboardRecheckListeners } from "./keyboard-event-listeners"; import { isPossibleKeyboardCloseEvent } from "./release-events"; import type { UseKeyboardHeightOptions, ViewportMetrics } from "./types"; import { computeFallbackLift, computeTargetOverlap, formatMetrics, getFocusedDurationMs, getLiftSource, getReleaseSkipReason, readViewportMetrics, shouldReleaseKeyboardState, } from "./viewport-metrics"; const log = new Logger("HooksUseKeyboardHeight"); export function useKeyboardHeight({ targetRef, active = false, onKeyboardDismiss, }: UseKeyboardHeightOptions = {}): void { const activeRef = useRef(active); const onKeyboardDismissRef = useRef(onKeyboardDismiss); const fallbackAllowedRef = useRef(false); const focusedAtRef = useRef(0); useEffect(() => { onKeyboardDismissRef.current = onKeyboardDismiss; }, [onKeyboardDismiss]); useEffect(() => { activeRef.current = active; fallbackAllowedRef.current = active; focusedAtRef.current = active ? performance.now() : 0; window.dispatchEvent(new Event(RECHECK_EVENT)); }, [active]); useEffect(() => { if (typeof window === "undefined") return; let rafId: number | null = null; let timeoutIds: number[] = []; let lastKeyboardHeight = -1; let lastViewportHeight = -1; let lastInputLift = -1; let lastMayBeKeyboardClose = false; let fallbackWasApplied = false; let lastEventType = "initial"; const environment = getKeyboardAdaptEnvironment(); log.debug("[keyboard] environment", environment); if (!shouldEnableKeyboardAdaptation(environment)) { resetKeyboardVars(); log.debug("[keyboard] disabled", { reason: "not-xiaomi-facebook-iab", environment, }); return; } const apply = () => { rafId = null; const metrics = readViewportMetrics(); let activeNow = isKeyboardTargetActive( targetRef?.current, activeRef.current, ); const shouldTryRelease = activeNow && lastMayBeKeyboardClose && shouldReleaseKeyboardState(metrics, focusedAtRef.current); if (activeNow && lastMayBeKeyboardClose && !shouldTryRelease) { log.debug("[keyboard] release skipped", { reason: getReleaseSkipReason(metrics, focusedAtRef.current), lastEventType, environment, active: activeNow, reactActive: activeRef.current, fallbackAllowed: fallbackAllowedRef.current, lastInputLift, fallbackWasApplied, focusedForMs: getFocusedDurationMs(focusedAtRef.current), metrics: formatMetrics(metrics), }); } if (shouldTryRelease) { log.debug("[keyboard] release confirmed", { lastEventType, environment, active: activeNow, reactActive: activeRef.current, fallbackAllowed: fallbackAllowedRef.current, lastInputLift, fallbackWasApplied, focusedForMs: getFocusedDurationMs(focusedAtRef.current), metrics: formatMetrics(metrics), }); releaseKeyboardState("keyboard-close-detected", metrics); activeNow = false; } lastMayBeKeyboardClose = false; const keyboardHeight = Math.round(metrics.keyboardHeight); const viewportHeight = Math.round(metrics.visibleHeight); const fallbackLift = fallbackAllowedRef.current ? computeFallbackLift(metrics) : 0; const inputLift = Math.round( activeNow ? Math.max( computeTargetOverlap(targetRef?.current, metrics.visibleBottom), fallbackLift, ) : 0, ); fallbackWasApplied = fallbackLift > 0 && inputLift >= fallbackLift && fallbackAllowedRef.current; if (keyboardHeight !== lastKeyboardHeight) { lastKeyboardHeight = keyboardHeight; setKeyboardHeightVar(keyboardHeight); } if (viewportHeight !== lastViewportHeight) { lastViewportHeight = viewportHeight; setViewportHeightVar(viewportHeight); } if (inputLift !== lastInputLift) { lastInputLift = inputLift; setInputLiftVar(inputLift); log.debug("[keyboard] apply", { environment, active: activeNow, reactActive: activeRef.current, inputLift, fallbackLift, fallbackAllowed: fallbackAllowedRef.current, source: getLiftSource(inputLift, fallbackLift), metrics: formatMetrics(metrics), }); } }; const schedule = () => { if (rafId != null) return; rafId = window.requestAnimationFrame(apply); }; const scheduleRechecks = (event?: Event) => { lastEventType = event?.type ?? "manual"; const hadInputLift = fallbackWasApplied || lastInputLift > 0; const possibleKeyboardClose = event != null && isPossibleKeyboardCloseEvent(event, hadInputLift); log.debug("[keyboard] schedule rechecks", { eventType: lastEventType, possibleKeyboardClose, hadInputLift, fallbackWasApplied, lastInputLift, active: activeRef.current, fallbackAllowed: fallbackAllowedRef.current, focusedForMs: getFocusedDurationMs(focusedAtRef.current), }); if (possibleKeyboardClose) { lastMayBeKeyboardClose = true; log.debug("[keyboard] possible close detected", { eventType: lastEventType, hadInputLift, fallbackWasApplied, lastInputLift, focusedForMs: getFocusedDurationMs(focusedAtRef.current), }); } timeoutIds.forEach((id) => window.clearTimeout(id)); timeoutIds = RECHECK_DELAYS_MS.map((delay) => window.setTimeout(schedule, delay), ); }; const releaseKeyboardState = ( reason: string, metrics: ViewportMetrics, ): void => { activeRef.current = false; fallbackAllowedRef.current = false; focusedAtRef.current = 0; blurKeyboardTarget(targetRef?.current); onKeyboardDismissRef.current?.(); log.debug("[keyboard] released", { reason, environment, metrics: formatMetrics(metrics), }); }; const removeKeyboardRecheckListeners = addKeyboardRecheckListeners(scheduleRechecks); scheduleRechecks(); return () => { if (rafId != null) window.cancelAnimationFrame(rafId); timeoutIds.forEach((id) => window.clearTimeout(id)); removeKeyboardRecheckListeners(); resetKeyboardVars(); }; }, [targetRef]); } export type { UseKeyboardHeightOptions };