diff --git a/lib/commonjs/components/KeyboardChatScrollView/index.js b/lib/commonjs/components/KeyboardChatScrollView/index.js index 0b78ff3..98b3b6c 100644 --- a/lib/commonjs/components/KeyboardChatScrollView/index.js +++ b/lib/commonjs/components/KeyboardChatScrollView/index.js @@ -28,9 +28,12 @@ const KeyboardChatScrollView = /*#__PURE__*/(0, _react.forwardRef)(({ offset = 0, extraContentPadding = ZERO_CONTENT_PADDING, blankSpace = ZERO_BLANK_SPACE, + adjustedInsetCompensation = 0, + adjustedStartInsetCompensation = 0, applyWorkaroundForContentInsetHitTestBug = false, onLayout: onLayoutProp, onContentSizeChange: onContentSizeChangeProp, + onContentInsetChange, onEndVisible, ...rest }, ref) => { @@ -54,6 +57,8 @@ const KeyboardChatScrollView = /*#__PURE__*/(0, _react.forwardRef)(({ offset, blankSpace, extraContentPadding, + adjustedInsetCompensation, + adjustedStartInsetCompensation, initialContentOffsetY: (_rest$contentOffset = rest.contentOffset) === null || _rest$contentOffset === void 0 ? void 0 : _rest$contentOffset.y }); (0, _useExtraContentPadding.useExtraContentPadding)({ @@ -61,6 +66,8 @@ const KeyboardChatScrollView = /*#__PURE__*/(0, _react.forwardRef)(({ extraContentPadding, keyboardPadding: padding, blankSpace, + adjustedInsetCompensation, + adjustedStartInsetCompensation, scroll, layout, size, @@ -91,10 +98,26 @@ const KeyboardChatScrollView = /*#__PURE__*/(0, _react.forwardRef)(({ // a bug for you, please open an issue. const totalPadding = (0, _reanimated.useDerivedValue)(() => Math.min(layout.value.height, Math.max(blankSpace.value, padding.value + extraContentPadding.value))); + // iOS applies the destination contentInset and contentOffset together at + // keyboard-animation start. Keep that native target behavior, but report + // the presentation height so virtualized-list layout follows the keyboard. + const reportedPadding = (0, _reanimated.useDerivedValue)(() => _reactNative.Platform.OS === "ios" ? Math.min(layout.value.height, Math.max(blankSpace.value, currentHeight.value + extraContentPadding.value)) : totalPadding.value); + + // Mirror the effective bottom padding (keyboard + composer + blank floor) + // to the consumer - a virtualized list needs it in its own scroll math or + // its end/maintain targets point at the under-the-keyboard resting offset. + (0, _reanimated.useAnimatedReaction)(() => reportedPadding.value, (current, previous) => { + if (onContentInsetChange && current !== previous) { + (0, _reactNativeReanimated.runOnJS)(onContentInsetChange)({ + bottom: current + }); + } + }, [onContentInsetChange]); + // Scroll indicator inset = keyboard + extraContentPadding (excludes blankSpace). // Apps that render into the unsafe area can supply a negative // scrollIndicatorInsets adjustment at the application layer. - const indicatorPadding = (0, _reanimated.useDerivedValue)(() => padding.value + extraContentPadding.value); + const indicatorPadding = (0, _reanimated.useDerivedValue)(() => padding.value); const onLayout = (0, _react.useCallback)(e => { onLayoutInternal(e); onLayoutProp === null || onLayoutProp === void 0 || onLayoutProp(e); diff --git a/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js b/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js index 4d33b3f..6a787d3 100644 --- a/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js +++ b/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js @@ -206,7 +206,7 @@ const clampedScrollTarget = (offsetBeforeScroll, keyboardHeight, contentHeight, * ``` */ exports.clampedScrollTarget = clampedScrollTarget; -const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentHeight, layoutHeight, inverted, totalPaddingForMaxScroll) => { +const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentHeight, layoutHeight, inverted, totalPaddingForMaxScroll, startInsetCompensation = 0) => { "worklet"; const paddingForMax = totalPaddingForMaxScroll !== undefined ? totalPaddingForMaxScroll : keyboardHeight; @@ -214,8 +214,9 @@ const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentHeight, const maxScroll = Math.max(contentHeight - layoutHeight, 0); return Math.max(Math.min(relativeScroll - keyboardHeight, maxScroll), -paddingForMax); } - const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, 0); - return Math.min(Math.max(keyboardHeight + relativeScroll, 0), maxScroll); + const minScroll = -startInsetCompensation; + const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, minScroll); + return Math.min(Math.max(keyboardHeight + relativeScroll, minScroll), maxScroll); }; exports.computeIOSContentOffset = computeIOSContentOffset; //# sourceMappingURL=helpers.js.map \ No newline at end of file diff --git a/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js b/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js index 6481228..0dc5dd1 100644 --- a/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js +++ b/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js @@ -32,7 +32,9 @@ function useChatKeyboard(scrollViewRef, options) { freeze, offset, blankSpace, - extraContentPadding + extraContentPadding, + adjustedInsetCompensation, + adjustedStartInsetCompensation } = options; const padding = (0, _reactNativeReanimated.useSharedValue)(0); const currentHeight = (0, _reactNativeReanimated.useSharedValue)(0); @@ -59,6 +61,9 @@ function useChatKeyboard(scrollViewRef, options) { targetKeyboardHeight.value = e.height; } const effective = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset); + if (e.duration === 0) { + currentHeight.value = effective; + } const atEnd = (0, _helpers.isScrollAtEnd)(scroll.value, layout.value.height, size.value.height, inverted); // Scale minimum padding absorption by how much of it is visible. @@ -67,7 +72,7 @@ function useChatKeyboard(scrollViewRef, options) { const visiblePadding = visibleFraction * blankSpace.value; const minimumPaddingAbsorbed = Math.max(0, visiblePadding - extraContentPadding.value); const scrollEffective = (0, _helpers.getScrollEffective)(effective, minimumPaddingAbsorbed); - const actualTotalPadding = Math.max(blankSpace.value, effective + extraContentPadding.value); + const actualTotalPadding = Math.max(blankSpace.value, effective + extraContentPadding.value) + adjustedInsetCompensation; // `onInteractive` owns the native scroll offset. When its final // keyboardWillHide event arrives, consume the marker so this @@ -87,8 +92,8 @@ function useChatKeyboard(scrollViewRef, options) { const maxScroll = Math.max(size.value.height - layout.value.height, 0); contentOffsetY.value = Math.max(-actualTotalPadding, Math.min(scroll.value, maxScroll)); } else { - const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, 0); - contentOffsetY.value = Math.max(0, Math.min(scroll.value, maxScroll)); + const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, -adjustedStartInsetCompensation); + contentOffsetY.value = Math.max(-adjustedStartInsetCompensation, Math.min(scroll.value, maxScroll)); } return; } @@ -102,8 +107,8 @@ function useChatKeyboard(scrollViewRef, options) { const maxScroll = Math.max(size.value.height - layout.value.height, 0); contentOffsetY.value = Math.max(-actualTotalPadding, Math.min(scroll.value, maxScroll)); } else { - const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, 0); - contentOffsetY.value = Math.max(0, Math.min(scroll.value, maxScroll)); + const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, -adjustedStartInsetCompensation); + contentOffsetY.value = Math.max(-adjustedStartInsetCompensation, Math.min(scroll.value, maxScroll)); } return; } @@ -129,17 +134,25 @@ function useChatKeyboard(scrollViewRef, options) { contentOffsetY.value = scroll.value; return; } - contentOffsetY.value = (0, _helpers.computeIOSContentOffset)(relativeScroll, scrollEffective, size.value.height, layout.value.height, inverted, actualTotalPadding); + contentOffsetY.value = (0, _helpers.computeIOSContentOffset)(relativeScroll, scrollEffective, size.value.height, layout.value.height, inverted, actualTotalPadding, adjustedStartInsetCompensation); }, - onInteractive: () => { + onMove: e => { "worklet"; - isInteractiveDismissal.value = true; + if (freeze.value) { + return; + } + currentHeight.value = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset); }, - onMove: () => { + onInteractive: e => { "worklet"; - // iOS doesn't need per-frame updates (contentOffset handles it) + isInteractiveDismissal.value = true; + + if (freeze.value) { + return; + } + currentHeight.value = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset); }, onEnd: e => { "worklet"; @@ -148,9 +161,10 @@ function useChatKeyboard(scrollViewRef, options) { return; } const effective = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset); + currentHeight.value = effective; padding.value = effective; } - }, [inverted, keyboardLiftBehavior, offset, extraContentPadding]); + }, [inverted, keyboardLiftBehavior, offset, extraContentPadding, adjustedInsetCompensation, adjustedStartInsetCompensation]); return { padding, currentHeight, diff --git a/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js b/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js index 11e81a9..de39eac 100644 --- a/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js +++ b/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js @@ -33,6 +33,8 @@ function useExtraContentPadding(options) { extraContentPadding, keyboardPadding, blankSpace, + adjustedInsetCompensation, + adjustedStartInsetCompensation, scroll, layout, size, @@ -74,8 +76,8 @@ function useExtraContentPadding(options) { } // Compute effective delta considering blankSpace floor - const previousTotal = Math.max(blankSpace.value, keyboardPadding.value + previous); - const currentTotal = Math.max(blankSpace.value, keyboardPadding.value + current); + const previousTotal = Math.max(blankSpace.value, keyboardPadding.value + previous) + adjustedInsetCompensation; + const currentTotal = Math.max(blankSpace.value, keyboardPadding.value + current) + adjustedInsetCompensation; const effectiveDelta = currentTotal - previousTotal; if (effectiveDelta === 0) { // blankSpace absorbed the change @@ -116,14 +118,15 @@ function useExtraContentPadding(options) { if (inverted) { target = Math.max(base - effectiveDelta, -currentTotal); } else { - const maxScroll = Math.max(size.value.height - layout.value.height + currentTotal, 0); + const minScroll = -adjustedStartInsetCompensation; + const maxScroll = Math.max(size.value.height - layout.value.height + currentTotal, minScroll); - // Clamp at 0 as well: when the content is shorter than the viewport a - // shrinking padding makes `effectiveDelta` negative while `base` is - // already 0, so the target would go below the top of the content. - target = Math.max(Math.min(base + effectiveDelta, maxScroll), 0); + // Clamp at the top as well: when the content is shorter than the + // viewport a shrinking padding makes `effectiveDelta` negative while + // `base` is already at the top. + target = Math.max(minScroll, Math.min(base + effectiveDelta, maxScroll)); } scrollToTarget(target); - }, [inverted, keyboardLiftBehavior]); + }, [inverted, keyboardLiftBehavior, adjustedInsetCompensation, adjustedStartInsetCompensation]); } //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/lib/commonjs/hooks/useKeyboardState/index.js b/lib/commonjs/hooks/useKeyboardState/index.js index a4b287006a47011a58c8a16f19b31a479087f44e..4084025d644c8555b269f9d496266e2cb3234024 100644 --- a/lib/commonjs/hooks/useKeyboardState/index.js +++ b/lib/commonjs/hooks/useKeyboardState/index.js @@ -7,7 +7,7 @@ exports.useKeyboardState = useKeyboardState; var _react = require("react"); var _bindings = require("../../bindings"); var _module = require("../../module"); -const EVENTS = ["keyboardWillShow", "keyboardDidHide"]; +const EVENTS = ["keyboardWillShow", "keyboardDidShow", "keyboardDidHide"]; const getLatestState = () => ({ ..._module.KeyboardController.state(), isVisible: _module.KeyboardController.isVisible() diff --git a/lib/commonjs/module.js b/lib/commonjs/module.js index f27c5a095129cf60831088a73ce94d44f3ba52cd..9b9477515cb4f1213bb2769e095efabe9e7ba58c 100644 --- a/lib/commonjs/module.js +++ b/lib/commonjs/module.js @@ -18,10 +18,13 @@ _bindings.KeyboardEvents.addListener("keyboardDidHide", e => { isClosed = true; lastState = e; }); -_bindings.KeyboardEvents.addListener("keyboardWillShow", e => { +const updateShownState = e => { isClosed = false; lastState = e; -}); +}; +_bindings.KeyboardEvents.addListener("keyboardWillShow", updateShownState); +// Android can restore IME insets without an animation and emit only didShow. +_bindings.KeyboardEvents.addListener("keyboardDidShow", updateShownState); const dismiss = async options => { const keepFocus = (options === null || options === void 0 ? void 0 : options.keepFocus) ?? false; const animated = (options === null || options === void 0 ? void 0 : options.animated) ?? true; diff --git a/lib/module/components/KeyboardChatScrollView/index.js b/lib/module/components/KeyboardChatScrollView/index.js index e0c8391..8df802a 100644 --- a/lib/module/components/KeyboardChatScrollView/index.js +++ b/lib/module/components/KeyboardChatScrollView/index.js @@ -1,9 +1,9 @@ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); } import React, { forwardRef, useCallback, useMemo } from "react"; -import { StyleSheet } from "react-native"; -import { makeMutable, useAnimatedRef } from "react-native-reanimated"; +import { Platform, StyleSheet } from "react-native"; +import { makeMutable, runOnJS, useAnimatedRef } from "react-native-reanimated"; import Reanimated from "react-native-reanimated"; -import { useAnimatedStyle, useDerivedValue } from "../../reanimated"; +import { useAnimatedReaction, useAnimatedStyle, useDerivedValue } from "../../reanimated"; import useCombinedRef from "../hooks/useCombinedRef"; import ScrollViewWithBottomPadding from "../ScrollViewWithBottomPadding"; import { useChatKeyboard } from "./useChatKeyboard"; @@ -21,9 +21,12 @@ const KeyboardChatScrollView = /*#__PURE__*/forwardRef(({ offset = 0, extraContentPadding = ZERO_CONTENT_PADDING, blankSpace = ZERO_BLANK_SPACE, + adjustedInsetCompensation = 0, + adjustedStartInsetCompensation = 0, applyWorkaroundForContentInsetHitTestBug = false, onLayout: onLayoutProp, onContentSizeChange: onContentSizeChangeProp, + onContentInsetChange, onEndVisible, ...rest }, ref) => { @@ -47,6 +50,8 @@ const KeyboardChatScrollView = /*#__PURE__*/forwardRef(({ offset, blankSpace, extraContentPadding, + adjustedInsetCompensation, + adjustedStartInsetCompensation, initialContentOffsetY: (_rest$contentOffset = rest.contentOffset) === null || _rest$contentOffset === void 0 ? void 0 : _rest$contentOffset.y }); useExtraContentPadding({ @@ -54,6 +59,8 @@ const KeyboardChatScrollView = /*#__PURE__*/forwardRef(({ extraContentPadding, keyboardPadding: padding, blankSpace, + adjustedInsetCompensation, + adjustedStartInsetCompensation, scroll, layout, size, @@ -84,10 +91,26 @@ const KeyboardChatScrollView = /*#__PURE__*/forwardRef(({ // a bug for you, please open an issue. const totalPadding = useDerivedValue(() => Math.min(layout.value.height, Math.max(blankSpace.value, padding.value + extraContentPadding.value))); + // iOS applies the destination contentInset and contentOffset together at + // keyboard-animation start. Keep that native target behavior, but report + // the presentation height so virtualized-list layout follows the keyboard. + const reportedPadding = useDerivedValue(() => Platform.OS === "ios" ? Math.min(layout.value.height, Math.max(blankSpace.value, currentHeight.value + extraContentPadding.value)) : totalPadding.value); + + // Mirror the effective bottom padding (keyboard + composer + blank floor) + // to the consumer - a virtualized list needs it in its own scroll math or + // its end/maintain targets point at the under-the-keyboard resting offset. + useAnimatedReaction(() => reportedPadding.value, (current, previous) => { + if (onContentInsetChange && current !== previous) { + runOnJS(onContentInsetChange)({ + bottom: current + }); + } + }, [onContentInsetChange]); + // Scroll indicator inset = keyboard + extraContentPadding (excludes blankSpace). // Apps that render into the unsafe area can supply a negative // scrollIndicatorInsets adjustment at the application layer. - const indicatorPadding = useDerivedValue(() => padding.value + extraContentPadding.value); + const indicatorPadding = useDerivedValue(() => padding.value); const onLayout = useCallback(e => { onLayoutInternal(e); onLayoutProp === null || onLayoutProp === void 0 || onLayoutProp(e); diff --git a/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js b/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js index 295e221..2f7b87e 100644 --- a/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js +++ b/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js @@ -193,7 +193,7 @@ export const clampedScrollTarget = (offsetBeforeScroll, keyboardHeight, contentH * computeIOSContentOffset(100, 300, 1000, 800, false); // 400 * ``` */ -export const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentHeight, layoutHeight, inverted, totalPaddingForMaxScroll) => { +export const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentHeight, layoutHeight, inverted, totalPaddingForMaxScroll, startInsetCompensation = 0) => { "worklet"; const paddingForMax = totalPaddingForMaxScroll !== undefined ? totalPaddingForMaxScroll : keyboardHeight; @@ -201,7 +201,8 @@ export const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentH const maxScroll = Math.max(contentHeight - layoutHeight, 0); return Math.max(Math.min(relativeScroll - keyboardHeight, maxScroll), -paddingForMax); } - const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, 0); - return Math.min(Math.max(keyboardHeight + relativeScroll, 0), maxScroll); + const minScroll = -startInsetCompensation; + const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, minScroll); + return Math.min(Math.max(keyboardHeight + relativeScroll, minScroll), maxScroll); }; //# sourceMappingURL=helpers.js.map \ No newline at end of file diff --git a/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js b/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js index 043b973..df5a408 100644 --- a/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js +++ b/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js @@ -25,7 +25,9 @@ function useChatKeyboard(scrollViewRef, options) { freeze, offset, blankSpace, - extraContentPadding + extraContentPadding, + adjustedInsetCompensation, + adjustedStartInsetCompensation } = options; const padding = useSharedValue(0); const currentHeight = useSharedValue(0); @@ -52,6 +54,9 @@ function useChatKeyboard(scrollViewRef, options) { targetKeyboardHeight.value = e.height; } const effective = getEffectiveHeight(e.height, targetKeyboardHeight.value, offset); + if (e.duration === 0) { + currentHeight.value = effective; + } const atEnd = isScrollAtEnd(scroll.value, layout.value.height, size.value.height, inverted); // Scale minimum padding absorption by how much of it is visible. @@ -60,7 +65,7 @@ function useChatKeyboard(scrollViewRef, options) { const visiblePadding = visibleFraction * blankSpace.value; const minimumPaddingAbsorbed = Math.max(0, visiblePadding - extraContentPadding.value); const scrollEffective = getScrollEffective(effective, minimumPaddingAbsorbed); - const actualTotalPadding = Math.max(blankSpace.value, effective + extraContentPadding.value); + const actualTotalPadding = Math.max(blankSpace.value, effective + extraContentPadding.value) + adjustedInsetCompensation; // `onInteractive` owns the native scroll offset. When its final // keyboardWillHide event arrives, consume the marker so this @@ -80,8 +85,8 @@ function useChatKeyboard(scrollViewRef, options) { const maxScroll = Math.max(size.value.height - layout.value.height, 0); contentOffsetY.value = Math.max(-actualTotalPadding, Math.min(scroll.value, maxScroll)); } else { - const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, 0); - contentOffsetY.value = Math.max(0, Math.min(scroll.value, maxScroll)); + const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, -adjustedStartInsetCompensation); + contentOffsetY.value = Math.max(-adjustedStartInsetCompensation, Math.min(scroll.value, maxScroll)); } return; } @@ -95,8 +100,8 @@ function useChatKeyboard(scrollViewRef, options) { const maxScroll = Math.max(size.value.height - layout.value.height, 0); contentOffsetY.value = Math.max(-actualTotalPadding, Math.min(scroll.value, maxScroll)); } else { - const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, 0); - contentOffsetY.value = Math.max(0, Math.min(scroll.value, maxScroll)); + const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, -adjustedStartInsetCompensation); + contentOffsetY.value = Math.max(-adjustedStartInsetCompensation, Math.min(scroll.value, maxScroll)); } return; } @@ -122,17 +127,25 @@ function useChatKeyboard(scrollViewRef, options) { contentOffsetY.value = scroll.value; return; } - contentOffsetY.value = computeIOSContentOffset(relativeScroll, scrollEffective, size.value.height, layout.value.height, inverted, actualTotalPadding); + contentOffsetY.value = computeIOSContentOffset(relativeScroll, scrollEffective, size.value.height, layout.value.height, inverted, actualTotalPadding, adjustedStartInsetCompensation); }, - onInteractive: () => { + onMove: e => { "worklet"; - isInteractiveDismissal.value = true; + if (freeze.value) { + return; + } + currentHeight.value = getEffectiveHeight(e.height, targetKeyboardHeight.value, offset); }, - onMove: () => { + onInteractive: e => { "worklet"; - // iOS doesn't need per-frame updates (contentOffset handles it) + isInteractiveDismissal.value = true; + + if (freeze.value) { + return; + } + currentHeight.value = getEffectiveHeight(e.height, targetKeyboardHeight.value, offset); }, onEnd: e => { "worklet"; @@ -141,9 +154,10 @@ function useChatKeyboard(scrollViewRef, options) { return; } const effective = getEffectiveHeight(e.height, targetKeyboardHeight.value, offset); + currentHeight.value = effective; padding.value = effective; } - }, [inverted, keyboardLiftBehavior, offset, extraContentPadding]); + }, [inverted, keyboardLiftBehavior, offset, extraContentPadding, adjustedInsetCompensation, adjustedStartInsetCompensation]); return { padding, currentHeight, diff --git a/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js b/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js index cdca47b..3ea3a7f 100644 --- a/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js +++ b/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js @@ -27,6 +27,8 @@ function useExtraContentPadding(options) { extraContentPadding, keyboardPadding, blankSpace, + adjustedInsetCompensation, + adjustedStartInsetCompensation, scroll, layout, size, @@ -68,8 +70,8 @@ function useExtraContentPadding(options) { } // Compute effective delta considering blankSpace floor - const previousTotal = Math.max(blankSpace.value, keyboardPadding.value + previous); - const currentTotal = Math.max(blankSpace.value, keyboardPadding.value + current); + const previousTotal = Math.max(blankSpace.value, keyboardPadding.value + previous) + adjustedInsetCompensation; + const currentTotal = Math.max(blankSpace.value, keyboardPadding.value + current) + adjustedInsetCompensation; const effectiveDelta = currentTotal - previousTotal; if (effectiveDelta === 0) { // blankSpace absorbed the change @@ -110,15 +112,16 @@ function useExtraContentPadding(options) { if (inverted) { target = Math.max(base - effectiveDelta, -currentTotal); } else { - const maxScroll = Math.max(size.value.height - layout.value.height + currentTotal, 0); + const minScroll = -adjustedStartInsetCompensation; + const maxScroll = Math.max(size.value.height - layout.value.height + currentTotal, minScroll); - // Clamp at 0 as well: when the content is shorter than the viewport a - // shrinking padding makes `effectiveDelta` negative while `base` is - // already 0, so the target would go below the top of the content. - target = Math.max(Math.min(base + effectiveDelta, maxScroll), 0); + // Clamp at the top as well: when the content is shorter than the + // viewport a shrinking padding makes `effectiveDelta` negative while + // `base` is already at the top. + target = Math.max(minScroll, Math.min(base + effectiveDelta, maxScroll)); } scrollToTarget(target); - }, [inverted, keyboardLiftBehavior]); + }, [inverted, keyboardLiftBehavior, adjustedInsetCompensation, adjustedStartInsetCompensation]); } export { useExtraContentPadding }; //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/lib/module/hooks/useKeyboardState/index.js b/lib/module/hooks/useKeyboardState/index.js index 8da7c09fbb398253420d7bfcd9a85abdb981065b..31b1ff4370d8328a0569e559e3bbfa4b201f5a9d 100644 --- a/lib/module/hooks/useKeyboardState/index.js +++ b/lib/module/hooks/useKeyboardState/index.js @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { KeyboardEvents } from "../../bindings"; import { KeyboardController } from "../../module"; -const EVENTS = ["keyboardWillShow", "keyboardDidHide"]; +const EVENTS = ["keyboardWillShow", "keyboardDidShow", "keyboardDidHide"]; const getLatestState = () => ({ ...KeyboardController.state(), isVisible: KeyboardController.isVisible() diff --git a/lib/module/module.js b/lib/module/module.js index 01aad2effee498a8daec55e1d4644a5fbc723765..36d08242a766a47cd254538f518e50a897012d94 100644 --- a/lib/module/module.js +++ b/lib/module/module.js @@ -12,10 +12,13 @@ KeyboardEvents.addListener("keyboardDidHide", e => { isClosed = true; lastState = e; }); -KeyboardEvents.addListener("keyboardWillShow", e => { +const updateShownState = e => { isClosed = false; lastState = e; -}); +}; +KeyboardEvents.addListener("keyboardWillShow", updateShownState); +// Android can restore IME insets without an animation and emit only didShow. +KeyboardEvents.addListener("keyboardDidShow", updateShownState); const dismiss = async options => { const keepFocus = (options === null || options === void 0 ? void 0 : options.keepFocus) ?? false; const animated = (options === null || options === void 0 ? void 0 : options.animated) ?? true; diff --git a/lib/typescript/components/KeyboardChatScrollView/types.d.ts b/lib/typescript/components/KeyboardChatScrollView/types.d.ts index eba1514..6597d80 100644 --- a/lib/typescript/components/KeyboardChatScrollView/types.d.ts +++ b/lib/typescript/components/KeyboardChatScrollView/types.d.ts @@ -88,6 +88,10 @@ export type KeyboardChatScrollViewProps = { * Default is `undefined` (equivalent to `0` — no minimum floor). */ blankSpace?: SharedValue; + /** Extra bottom inset UIKit adds beyond the raw contentInset (safe area). Offset math only. */ + adjustedInsetCompensation?: number; + /** Leading inset UIKit adds outside raw contentInset. Offset math only. */ + adjustedStartInsetCompensation?: number; /** * Fires whenever the effective content inset changes — the static `contentInset` * prop combined with the dynamic keyboard-driven padding. diff --git a/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/helpers.d.ts b/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/helpers.d.ts index 2b51cc4..3acec1f 100644 --- a/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/helpers.d.ts +++ b/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/helpers.d.ts @@ -129,4 +129,4 @@ export declare const clampedScrollTarget: (offsetBeforeScroll: number, keyboardH * computeIOSContentOffset(100, 300, 1000, 800, false); // 400 * ``` */ -export declare const computeIOSContentOffset: (relativeScroll: number, keyboardHeight: number, contentHeight: number, layoutHeight: number, inverted: boolean, totalPaddingForMaxScroll?: number) => number; +export declare const computeIOSContentOffset: (relativeScroll: number, keyboardHeight: number, contentHeight: number, layoutHeight: number, inverted: boolean, totalPaddingForMaxScroll?: number, startInsetCompensation?: number) => number; diff --git a/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts b/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts index 8d358bc..911f3d9 100644 --- a/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts +++ b/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts @@ -11,6 +11,10 @@ type UseChatKeyboardOptions = { blankSpace: SharedValue; /** Extra content padding shared value — needed on iOS to correctly clamp contentOffset. */ extraContentPadding: SharedValue; + /** Safe-area extra beyond raw contentInset. Offset math only. */ + adjustedInsetCompensation: number; + /** Leading inset UIKit adds beyond the raw contentInset. Offset math only. */ + adjustedStartInsetCompensation: number; }; type UseChatKeyboardReturn = { /** Extra scrollable space (= keyboard height). Used as contentInset on iOS, contentInsetBottom/contentInsetTop on Android. */ diff --git a/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts b/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts index ec73f70..45b3874 100644 --- a/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts +++ b/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts @@ -8,6 +8,10 @@ type UseExtraContentPaddingOptions = { keyboardPadding: SharedValue; /** Minimum inset floor — used to absorb keyboard and extraContentPadding changes. */ blankSpace: SharedValue; + /** Safe-area extra beyond raw contentInset. Offset math only. */ + adjustedInsetCompensation: number; + /** Leading inset UIKit adds beyond raw contentInset. Offset math only. */ + adjustedStartInsetCompensation: number; /** Current vertical scroll offset. */ scroll: SharedValue; /** Visible viewport dimensions. */ diff --git a/src/components/KeyboardChatScrollView/index.tsx b/src/components/KeyboardChatScrollView/index.tsx index 935bc71..d411dc4 100644 --- a/src/components/KeyboardChatScrollView/index.tsx +++ b/src/components/KeyboardChatScrollView/index.tsx @@ -1,9 +1,13 @@ import React, { forwardRef, useCallback, useMemo } from "react"; -import { StyleSheet } from "react-native"; -import { makeMutable, useAnimatedRef } from "react-native-reanimated"; +import { Platform, StyleSheet } from "react-native"; +import { makeMutable, runOnJS, useAnimatedRef } from "react-native-reanimated"; import Reanimated from "react-native-reanimated"; -import { useAnimatedStyle, useDerivedValue } from "../../reanimated"; +import { + useAnimatedReaction, + useAnimatedStyle, + useDerivedValue, +} from "../../reanimated"; import useCombinedRef from "../hooks/useCombinedRef"; import ScrollViewWithBottomPadding from "../ScrollViewWithBottomPadding"; @@ -35,9 +39,12 @@ const KeyboardChatScrollView: React.ForwardRefExoticComponent< offset = 0, extraContentPadding = ZERO_CONTENT_PADDING, blankSpace = ZERO_BLANK_SPACE, + adjustedInsetCompensation = 0, + adjustedStartInsetCompensation = 0, applyWorkaroundForContentInsetHitTestBug = false, onLayout: onLayoutProp, onContentSizeChange: onContentSizeChangeProp, + onContentInsetChange, onEndVisible, ...rest }, @@ -64,6 +71,8 @@ const KeyboardChatScrollView: React.ForwardRefExoticComponent< offset, blankSpace, extraContentPadding, + adjustedInsetCompensation, + adjustedStartInsetCompensation, initialContentOffsetY: rest.contentOffset?.y, }); @@ -72,6 +81,8 @@ const KeyboardChatScrollView: React.ForwardRefExoticComponent< extraContentPadding, keyboardPadding: padding, blankSpace, + adjustedInsetCompensation, + adjustedStartInsetCompensation, scroll, layout, size, @@ -109,13 +120,40 @@ const KeyboardChatScrollView: React.ForwardRefExoticComponent< ), ); - // Scroll indicator inset = keyboard + extraContentPadding (excludes blankSpace). - // Apps that render into the unsafe area can supply a negative - // scrollIndicatorInsets adjustment at the application layer. - const indicatorPadding = useDerivedValue( - () => padding.value + extraContentPadding.value, + // iOS applies the destination contentInset and contentOffset together at + // keyboard-animation start. Keep that native target behavior, but report + // the presentation height so virtualized-list layout follows the keyboard. + const reportedPadding = useDerivedValue(() => + Platform.OS === "ios" + ? Math.min( + layout.value.height, + Math.max( + blankSpace.value, + currentHeight.value + extraContentPadding.value, + ), + ) + : totalPadding.value, ); + // Mirror the effective bottom padding (keyboard + composer + blank floor) + // to the consumer — a virtualized list needs it in its own scroll math or + // its end/maintain targets point at the under-the-keyboard resting offset. + useAnimatedReaction( + () => reportedPadding.value, + (current, previous) => { + if (onContentInsetChange && current !== previous) { + runOnJS(onContentInsetChange)({ bottom: current }); + } + }, + [onContentInsetChange], + ); + + // Scroll indicator inset = keyboard only (excludes extraContentPadding and + // blankSpace): with a floating composer the indicator track should run the + // full height of the scroll view, behind the composer, like iOS Messages. + // The keyboard still lifts it so it never disappears under the keyboard. + const indicatorPadding = useDerivedValue(() => padding.value); + const onLayout = useCallback( (e: LayoutChangeEvent) => { onLayoutInternal(e); diff --git a/src/components/KeyboardChatScrollView/types.ts b/src/components/KeyboardChatScrollView/types.ts index b7c98c8..e1012a8 100644 --- a/src/components/KeyboardChatScrollView/types.ts +++ b/src/components/KeyboardChatScrollView/types.ts @@ -93,6 +93,17 @@ export type KeyboardChatScrollViewProps = { * Default is `undefined` (equivalent to `0` — no minimum floor). */ blankSpace?: SharedValue; + /** + * Extra bottom inset UIKit adds on top of the raw `contentInset` this + * component writes (e.g. the home-indicator safe area under + * `contentInsetAdjustmentBehavior="automatic"`). Used ONLY in scroll-offset + * math (max scroll / end pinning) — never written into `contentInset`. + * + * Default is `0`. + */ + adjustedInsetCompensation?: number; + /** Leading inset UIKit adds outside raw contentInset. Offset math only. */ + adjustedStartInsetCompensation?: number; /** * Fires whenever the effective content inset changes — the static `contentInset` * prop combined with the dynamic keyboard-driven padding. diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/helpers.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/helpers.ts index 37eacd1..3d77ab2 100644 --- a/src/components/KeyboardChatScrollView/useChatKeyboard/helpers.ts +++ b/src/components/KeyboardChatScrollView/useChatKeyboard/helpers.ts @@ -245,6 +245,7 @@ export const computeIOSContentOffset = ( layoutHeight: number, inverted: boolean, totalPaddingForMaxScroll?: number, + startInsetCompensation: number = 0, ): number => { "worklet"; @@ -262,7 +263,14 @@ export const computeIOSContentOffset = ( ); } - const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, 0); + const minScroll = -startInsetCompensation; + const maxScroll = Math.max( + contentHeight - layoutHeight + paddingForMax, + minScroll, + ); - return Math.min(Math.max(keyboardHeight + relativeScroll, 0), maxScroll); + return Math.min( + Math.max(keyboardHeight + relativeScroll, minScroll), + maxScroll, + ); }; diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts index 1ae9b81..208505b 100644 --- a/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts +++ b/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts @@ -43,6 +43,8 @@ function useChatKeyboard( offset, blankSpace, extraContentPadding, + adjustedInsetCompensation, + adjustedStartInsetCompensation, } = options; const padding = useSharedValue(0); @@ -80,6 +82,10 @@ function useChatKeyboard( offset, ); + if (e.duration === 0) { + currentHeight.value = effective; + } + const atEnd = isScrollAtEnd( scroll.value, layout.value.height, @@ -105,10 +111,12 @@ function useChatKeyboard( effective, minimumPaddingAbsorbed, ); - const actualTotalPadding = Math.max( - blankSpace.value, - effective + extraContentPadding.value, - ); + // UIKit adds adjustedInsetCompensation (safe area) on top of the raw + // inset; include it here so end/max-scroll targets match the real + // resting offsets. + const actualTotalPadding = + Math.max(blankSpace.value, effective + extraContentPadding.value) + + adjustedInsetCompensation; // `onInteractive` owns the native scroll offset. When its final // keyboardWillHide event arrives, consume the marker so this @@ -142,11 +150,11 @@ function useChatKeyboard( } else { const maxScroll = Math.max( size.value.height - layout.value.height + actualTotalPadding, - 0, + -adjustedStartInsetCompensation, ); contentOffsetY.value = Math.max( - 0, + -adjustedStartInsetCompensation, Math.min(scroll.value, maxScroll), ); } @@ -177,11 +185,11 @@ function useChatKeyboard( } else { const maxScroll = Math.max( size.value.height - layout.value.height + actualTotalPadding, - 0, + -adjustedStartInsetCompensation, ); contentOffsetY.value = Math.max( - 0, + -adjustedStartInsetCompensation, Math.min(scroll.value, maxScroll), ); } @@ -231,17 +239,36 @@ function useChatKeyboard( layout.value.height, inverted, actualTotalPadding, + adjustedStartInsetCompensation, ); }, - onInteractive: () => { + onMove: (e) => { "worklet"; - isInteractiveDismissal.value = true; + if (freeze.value) { + return; + } + + currentHeight.value = getEffectiveHeight( + e.height, + targetKeyboardHeight.value, + offset, + ); }, - onMove: () => { + onInteractive: (e) => { "worklet"; - // iOS doesn't need per-frame updates (contentOffset handles it) + isInteractiveDismissal.value = true; + + if (freeze.value) { + return; + } + + currentHeight.value = getEffectiveHeight( + e.height, + targetKeyboardHeight.value, + offset, + ); }, onEnd: (e) => { "worklet"; @@ -256,10 +283,18 @@ function useChatKeyboard( offset, ); + currentHeight.value = effective; padding.value = effective; }, }, - [inverted, keyboardLiftBehavior, offset, extraContentPadding], + [ + inverted, + keyboardLiftBehavior, + offset, + extraContentPadding, + adjustedInsetCompensation, + adjustedStartInsetCompensation, + ], ); return { diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts index f89b7c8..02e0e19 100644 --- a/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts +++ b/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts @@ -13,6 +13,10 @@ type UseChatKeyboardOptions = { blankSpace: SharedValue; /** Extra content padding shared value — needed on iOS to correctly clamp contentOffset. */ extraContentPadding: SharedValue; + /** Extra bottom inset UIKit adds beyond the raw contentInset (safe area). Offset math only. */ + adjustedInsetCompensation: number; + /** Leading inset UIKit adds beyond the raw contentInset. Offset math only. */ + adjustedStartInsetCompensation: number; }; type UseChatKeyboardReturn = { diff --git a/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts b/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts index 99cc6c6..d883e76 100644 --- a/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts +++ b/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts @@ -18,6 +18,10 @@ type UseExtraContentPaddingOptions = { keyboardPadding: SharedValue; /** Minimum inset floor — used to absorb keyboard and extraContentPadding changes. */ blankSpace: SharedValue; + /** Extra bottom inset UIKit adds beyond the raw contentInset (safe area). Offset math only. */ + adjustedInsetCompensation: number; + /** Leading inset UIKit adds beyond raw contentInset. Offset math only. */ + adjustedStartInsetCompensation: number; /** Current vertical scroll offset. */ scroll: SharedValue; /** Visible viewport dimensions. */ @@ -53,6 +57,8 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void { extraContentPadding, keyboardPadding, blankSpace, + adjustedInsetCompensation, + adjustedStartInsetCompensation, scroll, layout, size, @@ -104,14 +110,12 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void { } // Compute effective delta considering blankSpace floor - const previousTotal = Math.max( - blankSpace.value, - keyboardPadding.value + previous, - ); - const currentTotal = Math.max( - blankSpace.value, - keyboardPadding.value + current, - ); + const previousTotal = + Math.max(blankSpace.value, keyboardPadding.value + previous) + + adjustedInsetCompensation; + const currentTotal = + Math.max(blankSpace.value, keyboardPadding.value + current) + + adjustedInsetCompensation; const effectiveDelta = currentTotal - previousTotal; if (effectiveDelta === 0) { @@ -170,20 +174,26 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void { if (inverted) { target = Math.max(base - effectiveDelta, -currentTotal); } else { + const minScroll = -adjustedStartInsetCompensation; const maxScroll = Math.max( size.value.height - layout.value.height + currentTotal, - 0, + minScroll, ); - // Clamp at 0 as well: when the content is shorter than the viewport a - // shrinking padding makes `effectiveDelta` negative while `base` is - // already 0, so the target would go below the top of the content. - target = Math.max(Math.min(base + effectiveDelta, maxScroll), 0); + // Clamp at the top as well: when the content is shorter than the + // viewport a shrinking padding makes `effectiveDelta` negative while + // `base` is already at the top. + target = Math.max(minScroll, Math.min(base + effectiveDelta, maxScroll)); } scrollToTarget(target); }, - [inverted, keyboardLiftBehavior], + [ + inverted, + keyboardLiftBehavior, + adjustedInsetCompensation, + adjustedStartInsetCompensation, + ], ); } diff --git a/src/hooks/useKeyboardState/index.ts b/src/hooks/useKeyboardState/index.ts index e5fe38dd8e7b8370bfb575296259933094857ca5..d0b98f60a6d7ad7f72ebd70b22e1ce6b7fb56874 100644 --- a/src/hooks/useKeyboardState/index.ts +++ b/src/hooks/useKeyboardState/index.ts @@ -5,7 +5,7 @@ import { KeyboardController } from "../../module"; import type { IKeyboardState } from "../../types"; -const EVENTS = ["keyboardWillShow", "keyboardDidHide"] as const; +const EVENTS = ["keyboardWillShow", "keyboardDidShow", "keyboardDidHide"] as const; const getLatestState = () => ({ ...KeyboardController.state(), diff --git a/src/module.ts b/src/module.ts index f74941914c9ecebbd0a6c2ba55c5436378de79d4..317f9908d88cd2812567c079611d4884807b713d 100644 --- a/src/module.ts +++ b/src/module.ts @@ -21,10 +21,14 @@ KeyboardEvents.addListener("keyboardDidHide", (e) => { lastState = e; }); -KeyboardEvents.addListener("keyboardWillShow", (e) => { +const updateShownState = (e: KeyboardEventData) => { isClosed = false; lastState = e; -}); +}; + +KeyboardEvents.addListener("keyboardWillShow", updateShownState); +// Android can restore IME insets without an animation and emit only didShow. +KeyboardEvents.addListener("keyboardDidShow", updateShownState); const dismiss = async (options?: Partial): Promise => { const keepFocus = options?.keepFocus ?? false;