| author | |
| committer | |
| log | 1945ce82c0d66d2b8ffbe867e688c0ecbf530e69 |
| tree | c9a7903548dd6dd16cee69571dd347d9e681a8f4 |
| parent | f391794a35c604d57e166a3ab48d56fc6e4e469a |
| signature | Signed by PGP key B5690EEEBB952194 |
5 files changed, 961 insertions(+), 942 deletions(-)
apps/mobile/package.json+1-1| ... | @@ -114,7 +114,7 @@ | ... | @@ -114,7 +114,7 @@ |
| 114 | "react-native": "0.88.0-rc.3", | 114 | "react-native": "0.88.0-rc.3", |
| 115 | "react-native-gesture-handler": "~3.2.1", | 115 | "react-native-gesture-handler": "~3.2.1", |
| 116 | "react-native-image-viewing": "^0.2.2", | 116 | "react-native-image-viewing": "^0.2.2", |
| 117 | "react-native-keyboard-controller": "1.22.4", | 117 | "react-native-keyboard-controller": "1.22.6", |
| 118 | "react-native-nitro-markdown": "^0.5.0", | 118 | "react-native-nitro-markdown": "^0.5.0", |
| 119 | "react-native-nitro-modules": "0.35.9", | 119 | "react-native-nitro-modules": "0.35.9", |
| 120 | "react-native-reanimated": "4.7.0", | 120 | "react-native-reanimated": "4.7.0", |
patches/react-native-keyboard-controller@1.22.4.patch deleted-933| ... | @@ -1,933 +0,0 @@ | ||
| 1 | diff --git a/lib/commonjs/components/KeyboardChatScrollView/index.js b/lib/commonjs/components/KeyboardChatScrollView/index.js | ||
| 2 | index 5617394..4f2222a 100644 | ||
| 3 | --- a/lib/commonjs/components/KeyboardChatScrollView/index.js | ||
| 4 | +++ b/lib/commonjs/components/KeyboardChatScrollView/index.js | ||
| 5 | @@ -27,9 +27,12 @@ const KeyboardChatScrollView = /*#__PURE__*/(0, _react.forwardRef)(({ | ||
| 6 | offset = 0, | ||
| 7 | extraContentPadding = ZERO_CONTENT_PADDING, | ||
| 8 | blankSpace = ZERO_BLANK_SPACE, | ||
| 9 | + adjustedInsetCompensation = 0, | ||
| 10 | + adjustedStartInsetCompensation = 0, | ||
| 11 | applyWorkaroundForContentInsetHitTestBug = false, | ||
| 12 | onLayout: onLayoutProp, | ||
| 13 | onContentSizeChange: onContentSizeChangeProp, | ||
| 14 | + onContentInsetChange, | ||
| 15 | onEndVisible, | ||
| 16 | ...rest | ||
| 17 | }, ref) => { | ||
| 18 | @@ -51,13 +54,17 @@ const KeyboardChatScrollView = /*#__PURE__*/(0, _react.forwardRef)(({ | ||
| 19 | freeze: freezeSV, | ||
| 20 | offset, | ||
| 21 | blankSpace, | ||
| 22 | - extraContentPadding | ||
| 23 | + extraContentPadding, | ||
| 24 | + adjustedInsetCompensation, | ||
| 25 | + adjustedStartInsetCompensation | ||
| 26 | }); | ||
| 27 | (0, _useExtraContentPadding.useExtraContentPadding)({ | ||
| 28 | scrollViewRef, | ||
| 29 | extraContentPadding, | ||
| 30 | keyboardPadding: padding, | ||
| 31 | blankSpace, | ||
| 32 | + adjustedInsetCompensation, | ||
| 33 | + adjustedStartInsetCompensation, | ||
| 34 | scroll, | ||
| 35 | layout, | ||
| 36 | size, | ||
| 37 | @@ -88,10 +95,26 @@ const KeyboardChatScrollView = /*#__PURE__*/(0, _react.forwardRef)(({ | ||
| 38 | // a bug for you, please open an issue. | ||
| 39 | const totalPadding = (0, _reactNativeReanimated.useDerivedValue)(() => Math.min(layout.value.height, Math.max(blankSpace.value, padding.value + extraContentPadding.value))); | ||
| 40 | |||
| 41 | + // iOS applies the destination contentInset and contentOffset together at | ||
| 42 | + // keyboard-animation start. Keep that native target behavior, but report | ||
| 43 | + // the presentation height so virtualized-list layout follows the keyboard. | ||
| 44 | + const reportedPadding = (0, _reactNativeReanimated.useDerivedValue)(() => _reactNative.Platform.OS === "ios" ? Math.min(layout.value.height, Math.max(blankSpace.value, currentHeight.value + extraContentPadding.value)) : totalPadding.value); | ||
| 45 | + | ||
| 46 | + // Mirror the effective bottom padding (keyboard + composer + blank floor) | ||
| 47 | + // to the consumer - a virtualized list needs it in its own scroll math or | ||
| 48 | + // its end/maintain targets point at the under-the-keyboard resting offset. | ||
| 49 | + (0, _reactNativeReanimated.useAnimatedReaction)(() => reportedPadding.value, (current, previous) => { | ||
| 50 | + if (onContentInsetChange && current !== previous) { | ||
| 51 | + (0, _reactNativeReanimated.runOnJS)(onContentInsetChange)({ | ||
| 52 | + bottom: current | ||
| 53 | + }); | ||
| 54 | + } | ||
| 55 | + }, [onContentInsetChange]); | ||
| 56 | + | ||
| 57 | // Scroll indicator inset = keyboard + extraContentPadding (excludes blankSpace). | ||
| 58 | // Apps that render into the unsafe area can supply a negative | ||
| 59 | // scrollIndicatorInsets adjustment at the application layer. | ||
| 60 | - const indicatorPadding = (0, _reactNativeReanimated.useDerivedValue)(() => padding.value + extraContentPadding.value); | ||
| 61 | + const indicatorPadding = (0, _reactNativeReanimated.useDerivedValue)(() => padding.value); | ||
| 62 | const onLayout = (0, _react.useCallback)(e => { | ||
| 63 | onLayoutInternal(e); | ||
| 64 | onLayoutProp === null || onLayoutProp === void 0 || onLayoutProp(e); | ||
| 65 | diff --git a/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js b/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js | ||
| 66 | index 4d33b3f..6a787d3 100644 | ||
| 67 | --- a/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js | ||
| 68 | +++ b/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js | ||
| 69 | @@ -206,7 +206,7 @@ const clampedScrollTarget = (offsetBeforeScroll, keyboardHeight, contentHeight, | ||
| 70 | * ``` | ||
| 71 | */ | ||
| 72 | exports.clampedScrollTarget = clampedScrollTarget; | ||
| 73 | -const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentHeight, layoutHeight, inverted, totalPaddingForMaxScroll) => { | ||
| 74 | +const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentHeight, layoutHeight, inverted, totalPaddingForMaxScroll, startInsetCompensation = 0) => { | ||
| 75 | "worklet"; | ||
| 76 | |||
| 77 | const paddingForMax = totalPaddingForMaxScroll !== undefined ? totalPaddingForMaxScroll : keyboardHeight; | ||
| 78 | @@ -214,8 +214,9 @@ const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentHeight, | ||
| 79 | const maxScroll = Math.max(contentHeight - layoutHeight, 0); | ||
| 80 | return Math.max(Math.min(relativeScroll - keyboardHeight, maxScroll), -paddingForMax); | ||
| 81 | } | ||
| 82 | - const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, 0); | ||
| 83 | - return Math.min(Math.max(keyboardHeight + relativeScroll, 0), maxScroll); | ||
| 84 | + const minScroll = -startInsetCompensation; | ||
| 85 | + const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, minScroll); | ||
| 86 | + return Math.min(Math.max(keyboardHeight + relativeScroll, minScroll), maxScroll); | ||
| 87 | }; | ||
| 88 | exports.computeIOSContentOffset = computeIOSContentOffset; | ||
| 89 | //# sourceMappingURL=helpers.js.map | ||
| 90 | \ No newline at end of file | ||
| 91 | diff --git a/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js b/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js | ||
| 92 | index cecee07..533a9e9 100644 | ||
| 93 | --- a/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js | ||
| 94 | +++ b/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js | ||
| 95 | @@ -32,7 +32,9 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 96 | freeze, | ||
| 97 | offset, | ||
| 98 | blankSpace, | ||
| 99 | - extraContentPadding | ||
| 100 | + extraContentPadding, | ||
| 101 | + adjustedInsetCompensation, | ||
| 102 | + adjustedStartInsetCompensation | ||
| 103 | } = options; | ||
| 104 | const padding = (0, _reactNativeReanimated.useSharedValue)(0); | ||
| 105 | const currentHeight = (0, _reactNativeReanimated.useSharedValue)(0); | ||
| 106 | @@ -59,6 +61,9 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 107 | targetKeyboardHeight.value = e.height; | ||
| 108 | } | ||
| 109 | const effective = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset); | ||
| 110 | + if (e.duration === 0) { | ||
| 111 | + currentHeight.value = effective; | ||
| 112 | + } | ||
| 113 | const atEnd = (0, _helpers.isScrollAtEnd)(scroll.value, layout.value.height, size.value.height, inverted); | ||
| 114 | |||
| 115 | // Scale minimum padding absorption by how much of it is visible. | ||
| 116 | @@ -67,7 +72,7 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 117 | const visiblePadding = visibleFraction * blankSpace.value; | ||
| 118 | const minimumPaddingAbsorbed = Math.max(0, visiblePadding - extraContentPadding.value); | ||
| 119 | const scrollEffective = (0, _helpers.getScrollEffective)(effective, minimumPaddingAbsorbed); | ||
| 120 | - const actualTotalPadding = Math.max(blankSpace.value, effective + extraContentPadding.value); | ||
| 121 | + const actualTotalPadding = Math.max(blankSpace.value, effective + extraContentPadding.value) + adjustedInsetCompensation; | ||
| 122 | |||
| 123 | // `onInteractive` owns the native scroll offset. When its final | ||
| 124 | // keyboardWillHide event arrives, consume the marker so this | ||
| 125 | @@ -87,8 +92,8 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 126 | const maxScroll = Math.max(size.value.height - layout.value.height, 0); | ||
| 127 | contentOffsetY.value = Math.max(-actualTotalPadding, Math.min(scroll.value, maxScroll)); | ||
| 128 | } else { | ||
| 129 | - const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, 0); | ||
| 130 | - contentOffsetY.value = Math.max(0, Math.min(scroll.value, maxScroll)); | ||
| 131 | + const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, -adjustedStartInsetCompensation); | ||
| 132 | + contentOffsetY.value = Math.max(-adjustedStartInsetCompensation, Math.min(scroll.value, maxScroll)); | ||
| 133 | } | ||
| 134 | return; | ||
| 135 | } | ||
| 136 | @@ -102,8 +107,8 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 137 | const maxScroll = Math.max(size.value.height - layout.value.height, 0); | ||
| 138 | contentOffsetY.value = Math.max(-actualTotalPadding, Math.min(scroll.value, maxScroll)); | ||
| 139 | } else { | ||
| 140 | - const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, 0); | ||
| 141 | - contentOffsetY.value = Math.max(0, Math.min(scroll.value, maxScroll)); | ||
| 142 | + const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, -adjustedStartInsetCompensation); | ||
| 143 | + contentOffsetY.value = Math.max(-adjustedStartInsetCompensation, Math.min(scroll.value, maxScroll)); | ||
| 144 | } | ||
| 145 | return; | ||
| 146 | } | ||
| 147 | @@ -129,17 +134,25 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 148 | contentOffsetY.value = scroll.value; | ||
| 149 | return; | ||
| 150 | } | ||
| 151 | - contentOffsetY.value = (0, _helpers.computeIOSContentOffset)(relativeScroll, scrollEffective, size.value.height, layout.value.height, inverted, actualTotalPadding); | ||
| 152 | + contentOffsetY.value = (0, _helpers.computeIOSContentOffset)(relativeScroll, scrollEffective, size.value.height, layout.value.height, inverted, actualTotalPadding, adjustedStartInsetCompensation); | ||
| 153 | }, | ||
| 154 | - onInteractive: () => { | ||
| 155 | + onMove: e => { | ||
| 156 | "worklet"; | ||
| 157 | |||
| 158 | - isInteractiveDismissal.value = true; | ||
| 159 | + if (freeze.value) { | ||
| 160 | + return; | ||
| 161 | + } | ||
| 162 | + currentHeight.value = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset); | ||
| 163 | }, | ||
| 164 | - onMove: () => { | ||
| 165 | + onInteractive: e => { | ||
| 166 | "worklet"; | ||
| 167 | |||
| 168 | - // iOS doesn't need per-frame updates (contentOffset handles it) | ||
| 169 | + isInteractiveDismissal.value = true; | ||
| 170 | + | ||
| 171 | + if (freeze.value) { | ||
| 172 | + return; | ||
| 173 | + } | ||
| 174 | + currentHeight.value = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset); | ||
| 175 | }, | ||
| 176 | onEnd: e => { | ||
| 177 | "worklet"; | ||
| 178 | @@ -148,9 +161,10 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 179 | return; | ||
| 180 | } | ||
| 181 | const effective = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset); | ||
| 182 | + currentHeight.value = effective; | ||
| 183 | padding.value = effective; | ||
| 184 | } | ||
| 185 | - }, [inverted, keyboardLiftBehavior, offset, extraContentPadding]); | ||
| 186 | + }, [inverted, keyboardLiftBehavior, offset, extraContentPadding, adjustedInsetCompensation, adjustedStartInsetCompensation]); | ||
| 187 | return { | ||
| 188 | padding, | ||
| 189 | currentHeight, | ||
| 190 | diff --git a/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js b/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js | ||
| 191 | index 54a1c57..00c2941 100644 | ||
| 192 | --- a/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js | ||
| 193 | +++ b/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js | ||
| 194 | @@ -31,6 +31,8 @@ function useExtraContentPadding(options) { | ||
| 195 | extraContentPadding, | ||
| 196 | keyboardPadding, | ||
| 197 | blankSpace, | ||
| 198 | + adjustedInsetCompensation, | ||
| 199 | + adjustedStartInsetCompensation, | ||
| 200 | scroll, | ||
| 201 | layout, | ||
| 202 | size, | ||
| 203 | @@ -50,8 +52,13 @@ function useExtraContentPadding(options) { | ||
| 204 | // otherwise the native ScrollView clamps to the old range. | ||
| 205 | requestAnimationFrame(() => { | ||
| 206 | // check that view is still mounted and ref is actual | ||
| 207 | - // otherwise it may lead to a crash | ||
| 208 | - if (!scrollViewRef()) { | ||
| 209 | + // otherwise it may lead to a crash. Reanimated 4.7+ passes the | ||
| 210 | + // ref to the UI runtime as a shareable holding the node in `.value`. | ||
| 211 | + const node = | ||
| 212 | + typeof scrollViewRef === "function" | ||
| 213 | + ? scrollViewRef() | ||
| 214 | + : scrollViewRef.value; | ||
| 215 | + if (!node) { | ||
| 216 | return; | ||
| 217 | } | ||
| 218 | (0, _reactNativeReanimated.scrollTo)(scrollViewRef, 0, target, false); | ||
| 219 | @@ -70,8 +77,8 @@ function useExtraContentPadding(options) { | ||
| 220 | } | ||
| 221 | |||
| 222 | // Compute effective delta considering blankSpace floor | ||
| 223 | - const previousTotal = Math.max(blankSpace.value, keyboardPadding.value + previous); | ||
| 224 | - const currentTotal = Math.max(blankSpace.value, keyboardPadding.value + current); | ||
| 225 | + const previousTotal = Math.max(blankSpace.value, keyboardPadding.value + previous) + adjustedInsetCompensation; | ||
| 226 | + const currentTotal = Math.max(blankSpace.value, keyboardPadding.value + current) + adjustedInsetCompensation; | ||
| 227 | const effectiveDelta = currentTotal - previousTotal; | ||
| 228 | if (effectiveDelta === 0) { | ||
| 229 | // blankSpace absorbed the change | ||
| 230 | @@ -90,10 +97,11 @@ function useExtraContentPadding(options) { | ||
| 231 | const target = Math.max(scroll.value - effectiveDelta, -currentTotal); | ||
| 232 | scrollToTarget(target); | ||
| 233 | } else { | ||
| 234 | - const maxScroll = Math.max(size.value.height - layout.value.height + currentTotal, 0); | ||
| 235 | - const target = Math.min(scroll.value + effectiveDelta, maxScroll); | ||
| 236 | + const minScroll = -adjustedStartInsetCompensation; | ||
| 237 | + const maxScroll = Math.max(size.value.height - layout.value.height + currentTotal, minScroll); | ||
| 238 | + const target = Math.max(minScroll, Math.min(scroll.value + effectiveDelta, maxScroll)); | ||
| 239 | scrollToTarget(target); | ||
| 240 | } | ||
| 241 | - }, [inverted, keyboardLiftBehavior]); | ||
| 242 | + }, [inverted, keyboardLiftBehavior, adjustedInsetCompensation, adjustedStartInsetCompensation]); | ||
| 243 | } | ||
| 244 | //# sourceMappingURL=index.js.map | ||
| 245 | \ No newline at end of file | ||
| 246 | diff --git a/lib/module/components/KeyboardChatScrollView/index.js b/lib/module/components/KeyboardChatScrollView/index.js | ||
| 247 | index d085394..a57c2f9 100644 | ||
| 248 | --- a/lib/module/components/KeyboardChatScrollView/index.js | ||
| 249 | +++ b/lib/module/components/KeyboardChatScrollView/index.js | ||
| 250 | @@ -1,7 +1,7 @@ | ||
| 251 | 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); } | ||
| 252 | import React, { forwardRef, useCallback, useMemo } from "react"; | ||
| 253 | -import { StyleSheet } from "react-native"; | ||
| 254 | -import { makeMutable, useAnimatedRef, useAnimatedStyle, useDerivedValue } from "react-native-reanimated"; | ||
| 255 | +import { Platform, StyleSheet } from "react-native"; | ||
| 256 | +import { makeMutable, runOnJS, useAnimatedReaction, useAnimatedRef, useAnimatedStyle, useDerivedValue } from "react-native-reanimated"; | ||
| 257 | import Reanimated from "react-native-reanimated"; | ||
| 258 | import useCombinedRef from "../hooks/useCombinedRef"; | ||
| 259 | import ScrollViewWithBottomPadding from "../ScrollViewWithBottomPadding"; | ||
| 260 | @@ -20,9 +20,12 @@ const KeyboardChatScrollView = /*#__PURE__*/forwardRef(({ | ||
| 261 | offset = 0, | ||
| 262 | extraContentPadding = ZERO_CONTENT_PADDING, | ||
| 263 | blankSpace = ZERO_BLANK_SPACE, | ||
| 264 | + adjustedInsetCompensation = 0, | ||
| 265 | + adjustedStartInsetCompensation = 0, | ||
| 266 | applyWorkaroundForContentInsetHitTestBug = false, | ||
| 267 | onLayout: onLayoutProp, | ||
| 268 | onContentSizeChange: onContentSizeChangeProp, | ||
| 269 | + onContentInsetChange, | ||
| 270 | onEndVisible, | ||
| 271 | ...rest | ||
| 272 | }, ref) => { | ||
| 273 | @@ -44,13 +47,17 @@ const KeyboardChatScrollView = /*#__PURE__*/forwardRef(({ | ||
| 274 | freeze: freezeSV, | ||
| 275 | offset, | ||
| 276 | blankSpace, | ||
| 277 | - extraContentPadding | ||
| 278 | + extraContentPadding, | ||
| 279 | + adjustedInsetCompensation, | ||
| 280 | + adjustedStartInsetCompensation | ||
| 281 | }); | ||
| 282 | useExtraContentPadding({ | ||
| 283 | scrollViewRef, | ||
| 284 | extraContentPadding, | ||
| 285 | keyboardPadding: padding, | ||
| 286 | blankSpace, | ||
| 287 | + adjustedInsetCompensation, | ||
| 288 | + adjustedStartInsetCompensation, | ||
| 289 | scroll, | ||
| 290 | layout, | ||
| 291 | size, | ||
| 292 | @@ -81,10 +88,26 @@ const KeyboardChatScrollView = /*#__PURE__*/forwardRef(({ | ||
| 293 | // a bug for you, please open an issue. | ||
| 294 | const totalPadding = useDerivedValue(() => Math.min(layout.value.height, Math.max(blankSpace.value, padding.value + extraContentPadding.value))); | ||
| 295 | |||
| 296 | + // iOS applies the destination contentInset and contentOffset together at | ||
| 297 | + // keyboard-animation start. Keep that native target behavior, but report | ||
| 298 | + // the presentation height so virtualized-list layout follows the keyboard. | ||
| 299 | + const reportedPadding = useDerivedValue(() => Platform.OS === "ios" ? Math.min(layout.value.height, Math.max(blankSpace.value, currentHeight.value + extraContentPadding.value)) : totalPadding.value); | ||
| 300 | + | ||
| 301 | + // Mirror the effective bottom padding (keyboard + composer + blank floor) | ||
| 302 | + // to the consumer - a virtualized list needs it in its own scroll math or | ||
| 303 | + // its end/maintain targets point at the under-the-keyboard resting offset. | ||
| 304 | + useAnimatedReaction(() => reportedPadding.value, (current, previous) => { | ||
| 305 | + if (onContentInsetChange && current !== previous) { | ||
| 306 | + runOnJS(onContentInsetChange)({ | ||
| 307 | + bottom: current | ||
| 308 | + }); | ||
| 309 | + } | ||
| 310 | + }, [onContentInsetChange]); | ||
| 311 | + | ||
| 312 | // Scroll indicator inset = keyboard + extraContentPadding (excludes blankSpace). | ||
| 313 | // Apps that render into the unsafe area can supply a negative | ||
| 314 | // scrollIndicatorInsets adjustment at the application layer. | ||
| 315 | - const indicatorPadding = useDerivedValue(() => padding.value + extraContentPadding.value); | ||
| 316 | + const indicatorPadding = useDerivedValue(() => padding.value); | ||
| 317 | const onLayout = useCallback(e => { | ||
| 318 | onLayoutInternal(e); | ||
| 319 | onLayoutProp === null || onLayoutProp === void 0 || onLayoutProp(e); | ||
| 320 | diff --git a/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js b/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js | ||
| 321 | index 295e221..2f7b87e 100644 | ||
| 322 | --- a/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js | ||
| 323 | +++ b/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js | ||
| 324 | @@ -193,7 +193,7 @@ export const clampedScrollTarget = (offsetBeforeScroll, keyboardHeight, contentH | ||
| 325 | * computeIOSContentOffset(100, 300, 1000, 800, false); // 400 | ||
| 326 | * ``` | ||
| 327 | */ | ||
| 328 | -export const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentHeight, layoutHeight, inverted, totalPaddingForMaxScroll) => { | ||
| 329 | +export const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentHeight, layoutHeight, inverted, totalPaddingForMaxScroll, startInsetCompensation = 0) => { | ||
| 330 | "worklet"; | ||
| 331 | |||
| 332 | const paddingForMax = totalPaddingForMaxScroll !== undefined ? totalPaddingForMaxScroll : keyboardHeight; | ||
| 333 | @@ -201,7 +201,8 @@ export const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentH | ||
| 334 | const maxScroll = Math.max(contentHeight - layoutHeight, 0); | ||
| 335 | return Math.max(Math.min(relativeScroll - keyboardHeight, maxScroll), -paddingForMax); | ||
| 336 | } | ||
| 337 | - const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, 0); | ||
| 338 | - return Math.min(Math.max(keyboardHeight + relativeScroll, 0), maxScroll); | ||
| 339 | + const minScroll = -startInsetCompensation; | ||
| 340 | + const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, minScroll); | ||
| 341 | + return Math.min(Math.max(keyboardHeight + relativeScroll, minScroll), maxScroll); | ||
| 342 | }; | ||
| 343 | //# sourceMappingURL=helpers.js.map | ||
| 344 | \ No newline at end of file | ||
| 345 | diff --git a/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js b/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js | ||
| 346 | index 407275d..19427d7 100644 | ||
| 347 | --- a/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js | ||
| 348 | +++ b/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js | ||
| 349 | @@ -25,7 +25,9 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 350 | freeze, | ||
| 351 | offset, | ||
| 352 | blankSpace, | ||
| 353 | - extraContentPadding | ||
| 354 | + extraContentPadding, | ||
| 355 | + adjustedInsetCompensation, | ||
| 356 | + adjustedStartInsetCompensation | ||
| 357 | } = options; | ||
| 358 | const padding = useSharedValue(0); | ||
| 359 | const currentHeight = useSharedValue(0); | ||
| 360 | @@ -52,6 +54,9 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 361 | targetKeyboardHeight.value = e.height; | ||
| 362 | } | ||
| 363 | const effective = getEffectiveHeight(e.height, targetKeyboardHeight.value, offset); | ||
| 364 | + if (e.duration === 0) { | ||
| 365 | + currentHeight.value = effective; | ||
| 366 | + } | ||
| 367 | const atEnd = isScrollAtEnd(scroll.value, layout.value.height, size.value.height, inverted); | ||
| 368 | |||
| 369 | // Scale minimum padding absorption by how much of it is visible. | ||
| 370 | @@ -60,7 +65,7 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 371 | const visiblePadding = visibleFraction * blankSpace.value; | ||
| 372 | const minimumPaddingAbsorbed = Math.max(0, visiblePadding - extraContentPadding.value); | ||
| 373 | const scrollEffective = getScrollEffective(effective, minimumPaddingAbsorbed); | ||
| 374 | - const actualTotalPadding = Math.max(blankSpace.value, effective + extraContentPadding.value); | ||
| 375 | + const actualTotalPadding = Math.max(blankSpace.value, effective + extraContentPadding.value) + adjustedInsetCompensation; | ||
| 376 | |||
| 377 | // `onInteractive` owns the native scroll offset. When its final | ||
| 378 | // keyboardWillHide event arrives, consume the marker so this | ||
| 379 | @@ -80,8 +85,8 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 380 | const maxScroll = Math.max(size.value.height - layout.value.height, 0); | ||
| 381 | contentOffsetY.value = Math.max(-actualTotalPadding, Math.min(scroll.value, maxScroll)); | ||
| 382 | } else { | ||
| 383 | - const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, 0); | ||
| 384 | - contentOffsetY.value = Math.max(0, Math.min(scroll.value, maxScroll)); | ||
| 385 | + const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, -adjustedStartInsetCompensation); | ||
| 386 | + contentOffsetY.value = Math.max(-adjustedStartInsetCompensation, Math.min(scroll.value, maxScroll)); | ||
| 387 | } | ||
| 388 | return; | ||
| 389 | } | ||
| 390 | @@ -95,8 +100,8 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 391 | const maxScroll = Math.max(size.value.height - layout.value.height, 0); | ||
| 392 | contentOffsetY.value = Math.max(-actualTotalPadding, Math.min(scroll.value, maxScroll)); | ||
| 393 | } else { | ||
| 394 | - const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, 0); | ||
| 395 | - contentOffsetY.value = Math.max(0, Math.min(scroll.value, maxScroll)); | ||
| 396 | + const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, -adjustedStartInsetCompensation); | ||
| 397 | + contentOffsetY.value = Math.max(-adjustedStartInsetCompensation, Math.min(scroll.value, maxScroll)); | ||
| 398 | } | ||
| 399 | return; | ||
| 400 | } | ||
| 401 | @@ -122,17 +127,25 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 402 | contentOffsetY.value = scroll.value; | ||
| 403 | return; | ||
| 404 | } | ||
| 405 | - contentOffsetY.value = computeIOSContentOffset(relativeScroll, scrollEffective, size.value.height, layout.value.height, inverted, actualTotalPadding); | ||
| 406 | + contentOffsetY.value = computeIOSContentOffset(relativeScroll, scrollEffective, size.value.height, layout.value.height, inverted, actualTotalPadding, adjustedStartInsetCompensation); | ||
| 407 | }, | ||
| 408 | - onInteractive: () => { | ||
| 409 | + onMove: e => { | ||
| 410 | "worklet"; | ||
| 411 | |||
| 412 | - isInteractiveDismissal.value = true; | ||
| 413 | + if (freeze.value) { | ||
| 414 | + return; | ||
| 415 | + } | ||
| 416 | + currentHeight.value = getEffectiveHeight(e.height, targetKeyboardHeight.value, offset); | ||
| 417 | }, | ||
| 418 | - onMove: () => { | ||
| 419 | + onInteractive: e => { | ||
| 420 | "worklet"; | ||
| 421 | |||
| 422 | - // iOS doesn't need per-frame updates (contentOffset handles it) | ||
| 423 | + isInteractiveDismissal.value = true; | ||
| 424 | + | ||
| 425 | + if (freeze.value) { | ||
| 426 | + return; | ||
| 427 | + } | ||
| 428 | + currentHeight.value = getEffectiveHeight(e.height, targetKeyboardHeight.value, offset); | ||
| 429 | }, | ||
| 430 | onEnd: e => { | ||
| 431 | "worklet"; | ||
| 432 | @@ -141,9 +154,10 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 433 | return; | ||
| 434 | } | ||
| 435 | const effective = getEffectiveHeight(e.height, targetKeyboardHeight.value, offset); | ||
| 436 | + currentHeight.value = effective; | ||
| 437 | padding.value = effective; | ||
| 438 | } | ||
| 439 | - }, [inverted, keyboardLiftBehavior, offset, extraContentPadding]); | ||
| 440 | + }, [inverted, keyboardLiftBehavior, offset, extraContentPadding, adjustedInsetCompensation, adjustedStartInsetCompensation]); | ||
| 441 | return { | ||
| 442 | padding, | ||
| 443 | currentHeight, | ||
| 444 | diff --git a/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js b/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js | ||
| 445 | index 5885415..8b3c4f0 100644 | ||
| 446 | --- a/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js | ||
| 447 | +++ b/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js | ||
| 448 | @@ -25,6 +25,8 @@ function useExtraContentPadding(options) { | ||
| 449 | extraContentPadding, | ||
| 450 | keyboardPadding, | ||
| 451 | blankSpace, | ||
| 452 | + adjustedInsetCompensation, | ||
| 453 | + adjustedStartInsetCompensation, | ||
| 454 | scroll, | ||
| 455 | layout, | ||
| 456 | size, | ||
| 457 | @@ -44,8 +46,13 @@ function useExtraContentPadding(options) { | ||
| 458 | // otherwise the native ScrollView clamps to the old range. | ||
| 459 | requestAnimationFrame(() => { | ||
| 460 | // check that view is still mounted and ref is actual | ||
| 461 | - // otherwise it may lead to a crash | ||
| 462 | - if (!scrollViewRef()) { | ||
| 463 | + // otherwise it may lead to a crash. Reanimated 4.7+ passes the | ||
| 464 | + // ref to the UI runtime as a shareable holding the node in `.value`. | ||
| 465 | + const node = | ||
| 466 | + typeof scrollViewRef === "function" | ||
| 467 | + ? scrollViewRef() | ||
| 468 | + : scrollViewRef.value; | ||
| 469 | + if (!node) { | ||
| 470 | return; | ||
| 471 | } | ||
| 472 | scrollTo(scrollViewRef, 0, target, false); | ||
| 473 | @@ -64,8 +71,8 @@ function useExtraContentPadding(options) { | ||
| 474 | } | ||
| 475 | |||
| 476 | // Compute effective delta considering blankSpace floor | ||
| 477 | - const previousTotal = Math.max(blankSpace.value, keyboardPadding.value + previous); | ||
| 478 | - const currentTotal = Math.max(blankSpace.value, keyboardPadding.value + current); | ||
| 479 | + const previousTotal = Math.max(blankSpace.value, keyboardPadding.value + previous) + adjustedInsetCompensation; | ||
| 480 | + const currentTotal = Math.max(blankSpace.value, keyboardPadding.value + current) + adjustedInsetCompensation; | ||
| 481 | const effectiveDelta = currentTotal - previousTotal; | ||
| 482 | if (effectiveDelta === 0) { | ||
| 483 | // blankSpace absorbed the change | ||
| 484 | @@ -84,11 +91,12 @@ function useExtraContentPadding(options) { | ||
| 485 | const target = Math.max(scroll.value - effectiveDelta, -currentTotal); | ||
| 486 | scrollToTarget(target); | ||
| 487 | } else { | ||
| 488 | - const maxScroll = Math.max(size.value.height - layout.value.height + currentTotal, 0); | ||
| 489 | - const target = Math.min(scroll.value + effectiveDelta, maxScroll); | ||
| 490 | + const minScroll = -adjustedStartInsetCompensation; | ||
| 491 | + const maxScroll = Math.max(size.value.height - layout.value.height + currentTotal, minScroll); | ||
| 492 | + const target = Math.max(minScroll, Math.min(scroll.value + effectiveDelta, maxScroll)); | ||
| 493 | scrollToTarget(target); | ||
| 494 | } | ||
| 495 | - }, [inverted, keyboardLiftBehavior]); | ||
| 496 | + }, [inverted, keyboardLiftBehavior, adjustedInsetCompensation, adjustedStartInsetCompensation]); | ||
| 497 | } | ||
| 498 | export { useExtraContentPadding }; | ||
| 499 | //# sourceMappingURL=index.js.map | ||
| 500 | \ No newline at end of file | ||
| 501 | diff --git a/lib/typescript/components/KeyboardChatScrollView/types.d.ts b/lib/typescript/components/KeyboardChatScrollView/types.d.ts | ||
| 502 | index eba1514..6597d80 100644 | ||
| 503 | --- a/lib/typescript/components/KeyboardChatScrollView/types.d.ts | ||
| 504 | +++ b/lib/typescript/components/KeyboardChatScrollView/types.d.ts | ||
| 505 | @@ -88,6 +88,10 @@ export type KeyboardChatScrollViewProps = { | ||
| 506 | * Default is `undefined` (equivalent to `0` — no minimum floor). | ||
| 507 | */ | ||
| 508 | blankSpace?: SharedValue<number>; | ||
| 509 | + /** Extra bottom inset UIKit adds beyond the raw contentInset (safe area). Offset math only. */ | ||
| 510 | + adjustedInsetCompensation?: number; | ||
| 511 | + /** Leading inset UIKit adds outside raw contentInset. Offset math only. */ | ||
| 512 | + adjustedStartInsetCompensation?: number; | ||
| 513 | /** | ||
| 514 | * Fires whenever the effective content inset changes — the static `contentInset` | ||
| 515 | * prop combined with the dynamic keyboard-driven padding. | ||
| 516 | diff --git a/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/helpers.d.ts b/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/helpers.d.ts | ||
| 517 | index 2b51cc4..3acec1f 100644 | ||
| 518 | --- a/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/helpers.d.ts | ||
| 519 | +++ b/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/helpers.d.ts | ||
| 520 | @@ -129,4 +129,4 @@ export declare const clampedScrollTarget: (offsetBeforeScroll: number, keyboardH | ||
| 521 | * computeIOSContentOffset(100, 300, 1000, 800, false); // 400 | ||
| 522 | * ``` | ||
| 523 | */ | ||
| 524 | -export declare const computeIOSContentOffset: (relativeScroll: number, keyboardHeight: number, contentHeight: number, layoutHeight: number, inverted: boolean, totalPaddingForMaxScroll?: number) => number; | ||
| 525 | +export declare const computeIOSContentOffset: (relativeScroll: number, keyboardHeight: number, contentHeight: number, layoutHeight: number, inverted: boolean, totalPaddingForMaxScroll?: number, startInsetCompensation?: number) => number; | ||
| 526 | diff --git a/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts b/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts | ||
| 527 | index aff9b5a..fdd8395 100644 | ||
| 528 | --- a/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts | ||
| 529 | +++ b/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts | ||
| 530 | @@ -9,6 +9,10 @@ type UseChatKeyboardOptions = { | ||
| 531 | blankSpace: SharedValue<number>; | ||
| 532 | /** Extra content padding shared value — needed on iOS to correctly clamp contentOffset. */ | ||
| 533 | extraContentPadding: SharedValue<number>; | ||
| 534 | + /** Safe-area extra beyond raw contentInset. Offset math only. */ | ||
| 535 | + adjustedInsetCompensation: number; | ||
| 536 | + /** Leading inset UIKit adds beyond the raw contentInset. Offset math only. */ | ||
| 537 | + adjustedStartInsetCompensation: number; | ||
| 538 | }; | ||
| 539 | type UseChatKeyboardReturn = { | ||
| 540 | /** Extra scrollable space (= keyboard height). Used as contentInset on iOS, contentInsetBottom/contentInsetTop on Android. */ | ||
| 541 | diff --git a/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts b/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts | ||
| 542 | index ec73f70..45b3874 100644 | ||
| 543 | --- a/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts | ||
| 544 | +++ b/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts | ||
| 545 | @@ -8,6 +8,10 @@ type UseExtraContentPaddingOptions = { | ||
| 546 | keyboardPadding: SharedValue<number>; | ||
| 547 | /** Minimum inset floor — used to absorb keyboard and extraContentPadding changes. */ | ||
| 548 | blankSpace: SharedValue<number>; | ||
| 549 | + /** Safe-area extra beyond raw contentInset. Offset math only. */ | ||
| 550 | + adjustedInsetCompensation: number; | ||
| 551 | + /** Leading inset UIKit adds beyond raw contentInset. Offset math only. */ | ||
| 552 | + adjustedStartInsetCompensation: number; | ||
| 553 | /** Current vertical scroll offset. */ | ||
| 554 | scroll: SharedValue<number>; | ||
| 555 | /** Visible viewport dimensions. */ | ||
| 556 | diff --git a/src/components/KeyboardChatScrollView/index.tsx b/src/components/KeyboardChatScrollView/index.tsx | ||
| 557 | index 554b38c..725d546 100644 | ||
| 558 | --- a/src/components/KeyboardChatScrollView/index.tsx | ||
| 559 | +++ b/src/components/KeyboardChatScrollView/index.tsx | ||
| 560 | @@ -1,7 +1,9 @@ | ||
| 561 | import React, { forwardRef, useCallback, useMemo } from "react"; | ||
| 562 | -import { StyleSheet } from "react-native"; | ||
| 563 | +import { Platform, StyleSheet } from "react-native"; | ||
| 564 | import { | ||
| 565 | makeMutable, | ||
| 566 | + runOnJS, | ||
| 567 | + useAnimatedReaction, | ||
| 568 | useAnimatedRef, | ||
| 569 | useAnimatedStyle, | ||
| 570 | useDerivedValue, | ||
| 571 | @@ -39,9 +41,12 @@ const KeyboardChatScrollView: React.ForwardRefExoticComponent< | ||
| 572 | offset = 0, | ||
| 573 | extraContentPadding = ZERO_CONTENT_PADDING, | ||
| 574 | blankSpace = ZERO_BLANK_SPACE, | ||
| 575 | + adjustedInsetCompensation = 0, | ||
| 576 | + adjustedStartInsetCompensation = 0, | ||
| 577 | applyWorkaroundForContentInsetHitTestBug = false, | ||
| 578 | onLayout: onLayoutProp, | ||
| 579 | onContentSizeChange: onContentSizeChangeProp, | ||
| 580 | + onContentInsetChange, | ||
| 581 | onEndVisible, | ||
| 582 | ...rest | ||
| 583 | }, | ||
| 584 | @@ -68,6 +73,8 @@ const KeyboardChatScrollView: React.ForwardRefExoticComponent< | ||
| 585 | offset, | ||
| 586 | blankSpace, | ||
| 587 | extraContentPadding, | ||
| 588 | + adjustedInsetCompensation, | ||
| 589 | + adjustedStartInsetCompensation, | ||
| 590 | }); | ||
| 591 | |||
| 592 | useExtraContentPadding({ | ||
| 593 | @@ -75,6 +82,8 @@ const KeyboardChatScrollView: React.ForwardRefExoticComponent< | ||
| 594 | extraContentPadding, | ||
| 595 | keyboardPadding: padding, | ||
| 596 | blankSpace, | ||
| 597 | + adjustedInsetCompensation, | ||
| 598 | + adjustedStartInsetCompensation, | ||
| 599 | scroll, | ||
| 600 | layout, | ||
| 601 | size, | ||
| 602 | @@ -112,13 +121,40 @@ const KeyboardChatScrollView: React.ForwardRefExoticComponent< | ||
| 603 | ), | ||
| 604 | ); | ||
| 605 | |||
| 606 | - // Scroll indicator inset = keyboard + extraContentPadding (excludes blankSpace). | ||
| 607 | - // Apps that render into the unsafe area can supply a negative | ||
| 608 | - // scrollIndicatorInsets adjustment at the application layer. | ||
| 609 | - const indicatorPadding = useDerivedValue( | ||
| 610 | - () => padding.value + extraContentPadding.value, | ||
| 611 | + // iOS applies the destination contentInset and contentOffset together at | ||
| 612 | + // keyboard-animation start. Keep that native target behavior, but report | ||
| 613 | + // the presentation height so virtualized-list layout follows the keyboard. | ||
| 614 | + const reportedPadding = useDerivedValue(() => | ||
| 615 | + Platform.OS === "ios" | ||
| 616 | + ? Math.min( | ||
| 617 | + layout.value.height, | ||
| 618 | + Math.max( | ||
| 619 | + blankSpace.value, | ||
| 620 | + currentHeight.value + extraContentPadding.value, | ||
| 621 | + ), | ||
| 622 | + ) | ||
| 623 | + : totalPadding.value, | ||
| 624 | ); | ||
| 625 | |||
| 626 | + // Mirror the effective bottom padding (keyboard + composer + blank floor) | ||
| 627 | + // to the consumer — a virtualized list needs it in its own scroll math or | ||
| 628 | + // its end/maintain targets point at the under-the-keyboard resting offset. | ||
| 629 | + useAnimatedReaction( | ||
| 630 | + () => reportedPadding.value, | ||
| 631 | + (current, previous) => { | ||
| 632 | + if (onContentInsetChange && current !== previous) { | ||
| 633 | + runOnJS(onContentInsetChange)({ bottom: current }); | ||
| 634 | + } | ||
| 635 | + }, | ||
| 636 | + [onContentInsetChange], | ||
| 637 | + ); | ||
| 638 | + | ||
| 639 | + // Scroll indicator inset = keyboard only (excludes extraContentPadding and | ||
| 640 | + // blankSpace): with a floating composer the indicator track should run the | ||
| 641 | + // full height of the scroll view, behind the composer, like iOS Messages. | ||
| 642 | + // The keyboard still lifts it so it never disappears under the keyboard. | ||
| 643 | + const indicatorPadding = useDerivedValue(() => padding.value); | ||
| 644 | + | ||
| 645 | const onLayout = useCallback( | ||
| 646 | (e: LayoutChangeEvent) => { | ||
| 647 | onLayoutInternal(e); | ||
| 648 | diff --git a/src/components/KeyboardChatScrollView/types.ts b/src/components/KeyboardChatScrollView/types.ts | ||
| 649 | index b7c98c8..e1012a8 100644 | ||
| 650 | --- a/src/components/KeyboardChatScrollView/types.ts | ||
| 651 | +++ b/src/components/KeyboardChatScrollView/types.ts | ||
| 652 | @@ -93,6 +93,17 @@ export type KeyboardChatScrollViewProps = { | ||
| 653 | * Default is `undefined` (equivalent to `0` — no minimum floor). | ||
| 654 | */ | ||
| 655 | blankSpace?: SharedValue<number>; | ||
| 656 | + /** | ||
| 657 | + * Extra bottom inset UIKit adds on top of the raw `contentInset` this | ||
| 658 | + * component writes (e.g. the home-indicator safe area under | ||
| 659 | + * `contentInsetAdjustmentBehavior="automatic"`). Used ONLY in scroll-offset | ||
| 660 | + * math (max scroll / end pinning) — never written into `contentInset`. | ||
| 661 | + * | ||
| 662 | + * Default is `0`. | ||
| 663 | + */ | ||
| 664 | + adjustedInsetCompensation?: number; | ||
| 665 | + /** Leading inset UIKit adds outside raw contentInset. Offset math only. */ | ||
| 666 | + adjustedStartInsetCompensation?: number; | ||
| 667 | /** | ||
| 668 | * Fires whenever the effective content inset changes — the static `contentInset` | ||
| 669 | * prop combined with the dynamic keyboard-driven padding. | ||
| 670 | diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/helpers.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/helpers.ts | ||
| 671 | index 37eacd1..3d77ab2 100644 | ||
| 672 | --- a/src/components/KeyboardChatScrollView/useChatKeyboard/helpers.ts | ||
| 673 | +++ b/src/components/KeyboardChatScrollView/useChatKeyboard/helpers.ts | ||
| 674 | @@ -245,6 +245,7 @@ export const computeIOSContentOffset = ( | ||
| 675 | layoutHeight: number, | ||
| 676 | inverted: boolean, | ||
| 677 | totalPaddingForMaxScroll?: number, | ||
| 678 | + startInsetCompensation: number = 0, | ||
| 679 | ): number => { | ||
| 680 | "worklet"; | ||
| 681 | |||
| 682 | @@ -262,7 +263,14 @@ export const computeIOSContentOffset = ( | ||
| 683 | ); | ||
| 684 | } | ||
| 685 | |||
| 686 | - const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, 0); | ||
| 687 | + const minScroll = -startInsetCompensation; | ||
| 688 | + const maxScroll = Math.max( | ||
| 689 | + contentHeight - layoutHeight + paddingForMax, | ||
| 690 | + minScroll, | ||
| 691 | + ); | ||
| 692 | |||
| 693 | - return Math.min(Math.max(keyboardHeight + relativeScroll, 0), maxScroll); | ||
| 694 | + return Math.min( | ||
| 695 | + Math.max(keyboardHeight + relativeScroll, minScroll), | ||
| 696 | + maxScroll, | ||
| 697 | + ); | ||
| 698 | }; | ||
| 699 | diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts | ||
| 700 | index 129a74b..168ce1a 100644 | ||
| 701 | --- a/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts | ||
| 702 | +++ b/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts | ||
| 703 | @@ -43,6 +43,8 @@ function useChatKeyboard( | ||
| 704 | offset, | ||
| 705 | blankSpace, | ||
| 706 | extraContentPadding, | ||
| 707 | + adjustedInsetCompensation, | ||
| 708 | + adjustedStartInsetCompensation, | ||
| 709 | } = options; | ||
| 710 | |||
| 711 | const padding = useSharedValue(0); | ||
| 712 | @@ -80,6 +82,10 @@ function useChatKeyboard( | ||
| 713 | offset, | ||
| 714 | ); | ||
| 715 | |||
| 716 | + if (e.duration === 0) { | ||
| 717 | + currentHeight.value = effective; | ||
| 718 | + } | ||
| 719 | + | ||
| 720 | const atEnd = isScrollAtEnd( | ||
| 721 | scroll.value, | ||
| 722 | layout.value.height, | ||
| 723 | @@ -105,10 +111,12 @@ function useChatKeyboard( | ||
| 724 | effective, | ||
| 725 | minimumPaddingAbsorbed, | ||
| 726 | ); | ||
| 727 | - const actualTotalPadding = Math.max( | ||
| 728 | - blankSpace.value, | ||
| 729 | - effective + extraContentPadding.value, | ||
| 730 | - ); | ||
| 731 | + // UIKit adds adjustedInsetCompensation (safe area) on top of the raw | ||
| 732 | + // inset; include it here so end/max-scroll targets match the real | ||
| 733 | + // resting offsets. | ||
| 734 | + const actualTotalPadding = | ||
| 735 | + Math.max(blankSpace.value, effective + extraContentPadding.value) + | ||
| 736 | + adjustedInsetCompensation; | ||
| 737 | |||
| 738 | // `onInteractive` owns the native scroll offset. When its final | ||
| 739 | // keyboardWillHide event arrives, consume the marker so this | ||
| 740 | @@ -142,11 +150,11 @@ function useChatKeyboard( | ||
| 741 | } else { | ||
| 742 | const maxScroll = Math.max( | ||
| 743 | size.value.height - layout.value.height + actualTotalPadding, | ||
| 744 | - 0, | ||
| 745 | + -adjustedStartInsetCompensation, | ||
| 746 | ); | ||
| 747 | |||
| 748 | contentOffsetY.value = Math.max( | ||
| 749 | - 0, | ||
| 750 | + -adjustedStartInsetCompensation, | ||
| 751 | Math.min(scroll.value, maxScroll), | ||
| 752 | ); | ||
| 753 | } | ||
| 754 | @@ -177,11 +185,11 @@ function useChatKeyboard( | ||
| 755 | } else { | ||
| 756 | const maxScroll = Math.max( | ||
| 757 | size.value.height - layout.value.height + actualTotalPadding, | ||
| 758 | - 0, | ||
| 759 | + -adjustedStartInsetCompensation, | ||
| 760 | ); | ||
| 761 | |||
| 762 | contentOffsetY.value = Math.max( | ||
| 763 | - 0, | ||
| 764 | + -adjustedStartInsetCompensation, | ||
| 765 | Math.min(scroll.value, maxScroll), | ||
| 766 | ); | ||
| 767 | } | ||
| 768 | @@ -231,17 +239,36 @@ function useChatKeyboard( | ||
| 769 | layout.value.height, | ||
| 770 | inverted, | ||
| 771 | actualTotalPadding, | ||
| 772 | + adjustedStartInsetCompensation, | ||
| 773 | ); | ||
| 774 | }, | ||
| 775 | - onInteractive: () => { | ||
| 776 | + onMove: (e) => { | ||
| 777 | "worklet"; | ||
| 778 | |||
| 779 | - isInteractiveDismissal.value = true; | ||
| 780 | + if (freeze.value) { | ||
| 781 | + return; | ||
| 782 | + } | ||
| 783 | + | ||
| 784 | + currentHeight.value = getEffectiveHeight( | ||
| 785 | + e.height, | ||
| 786 | + targetKeyboardHeight.value, | ||
| 787 | + offset, | ||
| 788 | + ); | ||
| 789 | }, | ||
| 790 | - onMove: () => { | ||
| 791 | + onInteractive: (e) => { | ||
| 792 | "worklet"; | ||
| 793 | |||
| 794 | - // iOS doesn't need per-frame updates (contentOffset handles it) | ||
| 795 | + isInteractiveDismissal.value = true; | ||
| 796 | + | ||
| 797 | + if (freeze.value) { | ||
| 798 | + return; | ||
| 799 | + } | ||
| 800 | + | ||
| 801 | + currentHeight.value = getEffectiveHeight( | ||
| 802 | + e.height, | ||
| 803 | + targetKeyboardHeight.value, | ||
| 804 | + offset, | ||
| 805 | + ); | ||
| 806 | }, | ||
| 807 | onEnd: (e) => { | ||
| 808 | "worklet"; | ||
| 809 | @@ -256,10 +283,18 @@ function useChatKeyboard( | ||
| 810 | offset, | ||
| 811 | ); | ||
| 812 | |||
| 813 | + currentHeight.value = effective; | ||
| 814 | padding.value = effective; | ||
| 815 | }, | ||
| 816 | }, | ||
| 817 | - [inverted, keyboardLiftBehavior, offset, extraContentPadding], | ||
| 818 | + [ | ||
| 819 | + inverted, | ||
| 820 | + keyboardLiftBehavior, | ||
| 821 | + offset, | ||
| 822 | + extraContentPadding, | ||
| 823 | + adjustedInsetCompensation, | ||
| 824 | + adjustedStartInsetCompensation, | ||
| 825 | + ], | ||
| 826 | ); | ||
| 827 | |||
| 828 | return { | ||
| 829 | diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts | ||
| 830 | index 02abf5c..4ee0ddd 100644 | ||
| 831 | --- a/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts | ||
| 832 | +++ b/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts | ||
| 833 | @@ -11,6 +11,10 @@ type UseChatKeyboardOptions = { | ||
| 834 | blankSpace: SharedValue<number>; | ||
| 835 | /** Extra content padding shared value — needed on iOS to correctly clamp contentOffset. */ | ||
| 836 | extraContentPadding: SharedValue<number>; | ||
| 837 | + /** Extra bottom inset UIKit adds beyond the raw contentInset (safe area). Offset math only. */ | ||
| 838 | + adjustedInsetCompensation: number; | ||
| 839 | + /** Leading inset UIKit adds beyond the raw contentInset. Offset math only. */ | ||
| 840 | + adjustedStartInsetCompensation: number; | ||
| 841 | }; | ||
| 842 | |||
| 843 | type UseChatKeyboardReturn = { | ||
| 844 | diff --git a/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts b/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts | ||
| 845 | index c9c93f3..02f95c0 100644 | ||
| 846 | --- a/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts | ||
| 847 | +++ b/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts | ||
| 848 | @@ -16,6 +16,10 @@ type UseExtraContentPaddingOptions = { | ||
| 849 | keyboardPadding: SharedValue<number>; | ||
| 850 | /** Minimum inset floor — used to absorb keyboard and extraContentPadding changes. */ | ||
| 851 | blankSpace: SharedValue<number>; | ||
| 852 | + /** Extra bottom inset UIKit adds beyond the raw contentInset (safe area). Offset math only. */ | ||
| 853 | + adjustedInsetCompensation: number; | ||
| 854 | + /** Leading inset UIKit adds beyond raw contentInset. Offset math only. */ | ||
| 855 | + adjustedStartInsetCompensation: number; | ||
| 856 | /** Current vertical scroll offset. */ | ||
| 857 | scroll: SharedValue<number>; | ||
| 858 | /** Visible viewport dimensions. */ | ||
| 859 | @@ -51,6 +55,8 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void { | ||
| 860 | extraContentPadding, | ||
| 861 | keyboardPadding, | ||
| 862 | blankSpace, | ||
| 863 | + adjustedInsetCompensation, | ||
| 864 | + adjustedStartInsetCompensation, | ||
| 865 | scroll, | ||
| 866 | layout, | ||
| 867 | size, | ||
| 868 | @@ -72,8 +78,13 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void { | ||
| 869 | // otherwise the native ScrollView clamps to the old range. | ||
| 870 | requestAnimationFrame(() => { | ||
| 871 | // check that view is still mounted and ref is actual | ||
| 872 | - // otherwise it may lead to a crash | ||
| 873 | - if (!scrollViewRef()) { | ||
| 874 | + // otherwise it may lead to a crash. Reanimated 4.7+ passes the | ||
| 875 | + // ref to the UI runtime as a shareable holding the node in `.value`. | ||
| 876 | + const node = | ||
| 877 | + typeof scrollViewRef === "function" | ||
| 878 | + ? scrollViewRef() | ||
| 879 | + : (scrollViewRef as unknown as { value: unknown }).value; | ||
| 880 | + if (!node) { | ||
| 881 | return; | ||
| 882 | } | ||
| 883 | scrollTo(scrollViewRef, 0, target, false); | ||
| 884 | @@ -99,14 +110,12 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void { | ||
| 885 | } | ||
| 886 | |||
| 887 | // Compute effective delta considering blankSpace floor | ||
| 888 | - const previousTotal = Math.max( | ||
| 889 | - blankSpace.value, | ||
| 890 | - keyboardPadding.value + previous, | ||
| 891 | - ); | ||
| 892 | - const currentTotal = Math.max( | ||
| 893 | - blankSpace.value, | ||
| 894 | - keyboardPadding.value + current, | ||
| 895 | - ); | ||
| 896 | + const previousTotal = | ||
| 897 | + Math.max(blankSpace.value, keyboardPadding.value + previous) + | ||
| 898 | + adjustedInsetCompensation; | ||
| 899 | + const currentTotal = | ||
| 900 | + Math.max(blankSpace.value, keyboardPadding.value + current) + | ||
| 901 | + adjustedInsetCompensation; | ||
| 902 | const effectiveDelta = currentTotal - previousTotal; | ||
| 903 | |||
| 904 | if (effectiveDelta === 0) { | ||
| 905 | @@ -139,16 +148,25 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void { | ||
| 906 | |||
| 907 | scrollToTarget(target); | ||
| 908 | } else { | ||
| 909 | + const minScroll = -adjustedStartInsetCompensation; | ||
| 910 | const maxScroll = Math.max( | ||
| 911 | size.value.height - layout.value.height + currentTotal, | ||
| 912 | - 0, | ||
| 913 | + minScroll, | ||
| 914 | + ); | ||
| 915 | + const target = Math.max( | ||
| 916 | + minScroll, | ||
| 917 | + Math.min(scroll.value + effectiveDelta, maxScroll), | ||
| 918 | ); | ||
| 919 | - const target = Math.min(scroll.value + effectiveDelta, maxScroll); | ||
| 920 | |||
| 921 | scrollToTarget(target); | ||
| 922 | } | ||
| 923 | }, | ||
| 924 | - [inverted, keyboardLiftBehavior], | ||
| 925 | + [ | ||
| 926 | + inverted, | ||
| 927 | + keyboardLiftBehavior, | ||
| 928 | + adjustedInsetCompensation, | ||
| 929 | + adjustedStartInsetCompensation, | ||
| 930 | + ], | ||
| 931 | ); | ||
| 932 | } | ||
| 933 | |||
patches/react-native-keyboard-controller@1.22.6.patch created+952| ... | @@ -0,0 +1,952 @@ | ||
| 1 | diff --git a/lib/commonjs/components/KeyboardChatScrollView/index.js b/lib/commonjs/components/KeyboardChatScrollView/index.js | ||
| 2 | index 0b78ff3..98b3b6c 100644 | ||
| 3 | --- a/lib/commonjs/components/KeyboardChatScrollView/index.js | ||
| 4 | +++ b/lib/commonjs/components/KeyboardChatScrollView/index.js | ||
| 5 | @@ -28,9 +28,12 @@ const KeyboardChatScrollView = /*#__PURE__*/(0, _react.forwardRef)(({ | ||
| 6 | offset = 0, | ||
| 7 | extraContentPadding = ZERO_CONTENT_PADDING, | ||
| 8 | blankSpace = ZERO_BLANK_SPACE, | ||
| 9 | + adjustedInsetCompensation = 0, | ||
| 10 | + adjustedStartInsetCompensation = 0, | ||
| 11 | applyWorkaroundForContentInsetHitTestBug = false, | ||
| 12 | onLayout: onLayoutProp, | ||
| 13 | onContentSizeChange: onContentSizeChangeProp, | ||
| 14 | + onContentInsetChange, | ||
| 15 | onEndVisible, | ||
| 16 | ...rest | ||
| 17 | }, ref) => { | ||
| 18 | @@ -54,6 +57,8 @@ const KeyboardChatScrollView = /*#__PURE__*/(0, _react.forwardRef)(({ | ||
| 19 | offset, | ||
| 20 | blankSpace, | ||
| 21 | extraContentPadding, | ||
| 22 | + adjustedInsetCompensation, | ||
| 23 | + adjustedStartInsetCompensation, | ||
| 24 | initialContentOffsetY: (_rest$contentOffset = rest.contentOffset) === null || _rest$contentOffset === void 0 ? void 0 : _rest$contentOffset.y | ||
| 25 | }); | ||
| 26 | (0, _useExtraContentPadding.useExtraContentPadding)({ | ||
| 27 | @@ -61,6 +66,8 @@ const KeyboardChatScrollView = /*#__PURE__*/(0, _react.forwardRef)(({ | ||
| 28 | extraContentPadding, | ||
| 29 | keyboardPadding: padding, | ||
| 30 | blankSpace, | ||
| 31 | + adjustedInsetCompensation, | ||
| 32 | + adjustedStartInsetCompensation, | ||
| 33 | scroll, | ||
| 34 | layout, | ||
| 35 | size, | ||
| 36 | @@ -91,10 +98,26 @@ const KeyboardChatScrollView = /*#__PURE__*/(0, _react.forwardRef)(({ | ||
| 37 | // a bug for you, please open an issue. | ||
| 38 | const totalPadding = (0, _reanimated.useDerivedValue)(() => Math.min(layout.value.height, Math.max(blankSpace.value, padding.value + extraContentPadding.value))); | ||
| 39 | |||
| 40 | + // iOS applies the destination contentInset and contentOffset together at | ||
| 41 | + // keyboard-animation start. Keep that native target behavior, but report | ||
| 42 | + // the presentation height so virtualized-list layout follows the keyboard. | ||
| 43 | + const reportedPadding = (0, _reanimated.useDerivedValue)(() => _reactNative.Platform.OS === "ios" ? Math.min(layout.value.height, Math.max(blankSpace.value, currentHeight.value + extraContentPadding.value)) : totalPadding.value); | ||
| 44 | + | ||
| 45 | + // Mirror the effective bottom padding (keyboard + composer + blank floor) | ||
| 46 | + // to the consumer - a virtualized list needs it in its own scroll math or | ||
| 47 | + // its end/maintain targets point at the under-the-keyboard resting offset. | ||
| 48 | + (0, _reanimated.useAnimatedReaction)(() => reportedPadding.value, (current, previous) => { | ||
| 49 | + if (onContentInsetChange && current !== previous) { | ||
| 50 | + (0, _reactNativeReanimated.runOnJS)(onContentInsetChange)({ | ||
| 51 | + bottom: current | ||
| 52 | + }); | ||
| 53 | + } | ||
| 54 | + }, [onContentInsetChange]); | ||
| 55 | + | ||
| 56 | // Scroll indicator inset = keyboard + extraContentPadding (excludes blankSpace). | ||
| 57 | // Apps that render into the unsafe area can supply a negative | ||
| 58 | // scrollIndicatorInsets adjustment at the application layer. | ||
| 59 | - const indicatorPadding = (0, _reanimated.useDerivedValue)(() => padding.value + extraContentPadding.value); | ||
| 60 | + const indicatorPadding = (0, _reanimated.useDerivedValue)(() => padding.value); | ||
| 61 | const onLayout = (0, _react.useCallback)(e => { | ||
| 62 | onLayoutInternal(e); | ||
| 63 | onLayoutProp === null || onLayoutProp === void 0 || onLayoutProp(e); | ||
| 64 | diff --git a/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js b/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js | ||
| 65 | index 4d33b3f..6a787d3 100644 | ||
| 66 | --- a/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js | ||
| 67 | +++ b/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js | ||
| 68 | @@ -206,7 +206,7 @@ const clampedScrollTarget = (offsetBeforeScroll, keyboardHeight, contentHeight, | ||
| 69 | * ``` | ||
| 70 | */ | ||
| 71 | exports.clampedScrollTarget = clampedScrollTarget; | ||
| 72 | -const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentHeight, layoutHeight, inverted, totalPaddingForMaxScroll) => { | ||
| 73 | +const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentHeight, layoutHeight, inverted, totalPaddingForMaxScroll, startInsetCompensation = 0) => { | ||
| 74 | "worklet"; | ||
| 75 | |||
| 76 | const paddingForMax = totalPaddingForMaxScroll !== undefined ? totalPaddingForMaxScroll : keyboardHeight; | ||
| 77 | @@ -214,8 +214,9 @@ const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentHeight, | ||
| 78 | const maxScroll = Math.max(contentHeight - layoutHeight, 0); | ||
| 79 | return Math.max(Math.min(relativeScroll - keyboardHeight, maxScroll), -paddingForMax); | ||
| 80 | } | ||
| 81 | - const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, 0); | ||
| 82 | - return Math.min(Math.max(keyboardHeight + relativeScroll, 0), maxScroll); | ||
| 83 | + const minScroll = -startInsetCompensation; | ||
| 84 | + const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, minScroll); | ||
| 85 | + return Math.min(Math.max(keyboardHeight + relativeScroll, minScroll), maxScroll); | ||
| 86 | }; | ||
| 87 | exports.computeIOSContentOffset = computeIOSContentOffset; | ||
| 88 | //# sourceMappingURL=helpers.js.map | ||
| 89 | \ No newline at end of file | ||
| 90 | diff --git a/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js b/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js | ||
| 91 | index 6481228..0dc5dd1 100644 | ||
| 92 | --- a/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js | ||
| 93 | +++ b/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js | ||
| 94 | @@ -32,7 +32,9 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 95 | freeze, | ||
| 96 | offset, | ||
| 97 | blankSpace, | ||
| 98 | - extraContentPadding | ||
| 99 | + extraContentPadding, | ||
| 100 | + adjustedInsetCompensation, | ||
| 101 | + adjustedStartInsetCompensation | ||
| 102 | } = options; | ||
| 103 | const padding = (0, _reactNativeReanimated.useSharedValue)(0); | ||
| 104 | const currentHeight = (0, _reactNativeReanimated.useSharedValue)(0); | ||
| 105 | @@ -59,6 +61,9 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 106 | targetKeyboardHeight.value = e.height; | ||
| 107 | } | ||
| 108 | const effective = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset); | ||
| 109 | + if (e.duration === 0) { | ||
| 110 | + currentHeight.value = effective; | ||
| 111 | + } | ||
| 112 | const atEnd = (0, _helpers.isScrollAtEnd)(scroll.value, layout.value.height, size.value.height, inverted); | ||
| 113 | |||
| 114 | // Scale minimum padding absorption by how much of it is visible. | ||
| 115 | @@ -67,7 +72,7 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 116 | const visiblePadding = visibleFraction * blankSpace.value; | ||
| 117 | const minimumPaddingAbsorbed = Math.max(0, visiblePadding - extraContentPadding.value); | ||
| 118 | const scrollEffective = (0, _helpers.getScrollEffective)(effective, minimumPaddingAbsorbed); | ||
| 119 | - const actualTotalPadding = Math.max(blankSpace.value, effective + extraContentPadding.value); | ||
| 120 | + const actualTotalPadding = Math.max(blankSpace.value, effective + extraContentPadding.value) + adjustedInsetCompensation; | ||
| 121 | |||
| 122 | // `onInteractive` owns the native scroll offset. When its final | ||
| 123 | // keyboardWillHide event arrives, consume the marker so this | ||
| 124 | @@ -87,8 +92,8 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 125 | const maxScroll = Math.max(size.value.height - layout.value.height, 0); | ||
| 126 | contentOffsetY.value = Math.max(-actualTotalPadding, Math.min(scroll.value, maxScroll)); | ||
| 127 | } else { | ||
| 128 | - const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, 0); | ||
| 129 | - contentOffsetY.value = Math.max(0, Math.min(scroll.value, maxScroll)); | ||
| 130 | + const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, -adjustedStartInsetCompensation); | ||
| 131 | + contentOffsetY.value = Math.max(-adjustedStartInsetCompensation, Math.min(scroll.value, maxScroll)); | ||
| 132 | } | ||
| 133 | return; | ||
| 134 | } | ||
| 135 | @@ -102,8 +107,8 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 136 | const maxScroll = Math.max(size.value.height - layout.value.height, 0); | ||
| 137 | contentOffsetY.value = Math.max(-actualTotalPadding, Math.min(scroll.value, maxScroll)); | ||
| 138 | } else { | ||
| 139 | - const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, 0); | ||
| 140 | - contentOffsetY.value = Math.max(0, Math.min(scroll.value, maxScroll)); | ||
| 141 | + const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, -adjustedStartInsetCompensation); | ||
| 142 | + contentOffsetY.value = Math.max(-adjustedStartInsetCompensation, Math.min(scroll.value, maxScroll)); | ||
| 143 | } | ||
| 144 | return; | ||
| 145 | } | ||
| 146 | @@ -129,17 +134,25 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 147 | contentOffsetY.value = scroll.value; | ||
| 148 | return; | ||
| 149 | } | ||
| 150 | - contentOffsetY.value = (0, _helpers.computeIOSContentOffset)(relativeScroll, scrollEffective, size.value.height, layout.value.height, inverted, actualTotalPadding); | ||
| 151 | + contentOffsetY.value = (0, _helpers.computeIOSContentOffset)(relativeScroll, scrollEffective, size.value.height, layout.value.height, inverted, actualTotalPadding, adjustedStartInsetCompensation); | ||
| 152 | }, | ||
| 153 | - onInteractive: () => { | ||
| 154 | + onMove: e => { | ||
| 155 | "worklet"; | ||
| 156 | |||
| 157 | - isInteractiveDismissal.value = true; | ||
| 158 | + if (freeze.value) { | ||
| 159 | + return; | ||
| 160 | + } | ||
| 161 | + currentHeight.value = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset); | ||
| 162 | }, | ||
| 163 | - onMove: () => { | ||
| 164 | + onInteractive: e => { | ||
| 165 | "worklet"; | ||
| 166 | |||
| 167 | - // iOS doesn't need per-frame updates (contentOffset handles it) | ||
| 168 | + isInteractiveDismissal.value = true; | ||
| 169 | + | ||
| 170 | + if (freeze.value) { | ||
| 171 | + return; | ||
| 172 | + } | ||
| 173 | + currentHeight.value = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset); | ||
| 174 | }, | ||
| 175 | onEnd: e => { | ||
| 176 | "worklet"; | ||
| 177 | @@ -148,9 +161,10 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 178 | return; | ||
| 179 | } | ||
| 180 | const effective = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset); | ||
| 181 | + currentHeight.value = effective; | ||
| 182 | padding.value = effective; | ||
| 183 | } | ||
| 184 | - }, [inverted, keyboardLiftBehavior, offset, extraContentPadding]); | ||
| 185 | + }, [inverted, keyboardLiftBehavior, offset, extraContentPadding, adjustedInsetCompensation, adjustedStartInsetCompensation]); | ||
| 186 | return { | ||
| 187 | padding, | ||
| 188 | currentHeight, | ||
| 189 | diff --git a/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js b/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js | ||
| 190 | index 0215bef..5307446 100644 | ||
| 191 | --- a/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js | ||
| 192 | +++ b/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js | ||
| 193 | @@ -32,6 +32,8 @@ function useExtraContentPadding(options) { | ||
| 194 | extraContentPadding, | ||
| 195 | keyboardPadding, | ||
| 196 | blankSpace, | ||
| 197 | + adjustedInsetCompensation, | ||
| 198 | + adjustedStartInsetCompensation, | ||
| 199 | scroll, | ||
| 200 | layout, | ||
| 201 | size, | ||
| 202 | @@ -51,8 +53,13 @@ function useExtraContentPadding(options) { | ||
| 203 | // otherwise the native ScrollView clamps to the old range. | ||
| 204 | requestAnimationFrame(() => { | ||
| 205 | // check that view is still mounted and ref is actual | ||
| 206 | - // otherwise it may lead to a crash | ||
| 207 | - if (!scrollViewRef()) { | ||
| 208 | + // otherwise it may lead to a crash. Reanimated 4.7+ passes the | ||
| 209 | + // ref to the UI runtime as a shareable holding the node in `.value`. | ||
| 210 | + const node = | ||
| 211 | + typeof scrollViewRef === "function" | ||
| 212 | + ? scrollViewRef() | ||
| 213 | + : scrollViewRef.value; | ||
| 214 | + if (!node) { | ||
| 215 | return; | ||
| 216 | } | ||
| 217 | (0, _reactNativeReanimated.scrollTo)(scrollViewRef, 0, target, false); | ||
| 218 | @@ -71,8 +78,8 @@ function useExtraContentPadding(options) { | ||
| 219 | } | ||
| 220 | |||
| 221 | // Compute effective delta considering blankSpace floor | ||
| 222 | - const previousTotal = Math.max(blankSpace.value, keyboardPadding.value + previous); | ||
| 223 | - const currentTotal = Math.max(blankSpace.value, keyboardPadding.value + current); | ||
| 224 | + const previousTotal = Math.max(blankSpace.value, keyboardPadding.value + previous) + adjustedInsetCompensation; | ||
| 225 | + const currentTotal = Math.max(blankSpace.value, keyboardPadding.value + current) + adjustedInsetCompensation; | ||
| 226 | const effectiveDelta = currentTotal - previousTotal; | ||
| 227 | if (effectiveDelta === 0) { | ||
| 228 | // blankSpace absorbed the change | ||
| 229 | @@ -91,13 +98,11 @@ function useExtraContentPadding(options) { | ||
| 230 | const target = Math.max(scroll.value - effectiveDelta, -currentTotal); | ||
| 231 | scrollToTarget(target); | ||
| 232 | } else { | ||
| 233 | - const maxScroll = Math.max(size.value.height - layout.value.height + currentTotal, 0); | ||
| 234 | - // Clamp at 0 as well: when the content is shorter than the viewport a | ||
| 235 | - // shrinking padding makes `effectiveDelta` negative while `scroll.value` | ||
| 236 | - // is already 0, so the target would go below the top of the content. | ||
| 237 | - const target = Math.max(Math.min(scroll.value + effectiveDelta, maxScroll), 0); | ||
| 238 | + const minScroll = -adjustedStartInsetCompensation; | ||
| 239 | + const maxScroll = Math.max(size.value.height - layout.value.height + currentTotal, minScroll); | ||
| 240 | + const target = Math.max(minScroll, Math.min(scroll.value + effectiveDelta, maxScroll)); | ||
| 241 | scrollToTarget(target); | ||
| 242 | } | ||
| 243 | - }, [inverted, keyboardLiftBehavior]); | ||
| 244 | + }, [inverted, keyboardLiftBehavior, adjustedInsetCompensation, adjustedStartInsetCompensation]); | ||
| 245 | } | ||
| 246 | //# sourceMappingURL=index.js.map | ||
| 247 | \ No newline at end of file | ||
| 248 | diff --git a/lib/module/components/KeyboardChatScrollView/index.js b/lib/module/components/KeyboardChatScrollView/index.js | ||
| 249 | index e0c8391..8df802a 100644 | ||
| 250 | --- a/lib/module/components/KeyboardChatScrollView/index.js | ||
| 251 | +++ b/lib/module/components/KeyboardChatScrollView/index.js | ||
| 252 | @@ -1,9 +1,9 @@ | ||
| 253 | 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); } | ||
| 254 | import React, { forwardRef, useCallback, useMemo } from "react"; | ||
| 255 | -import { StyleSheet } from "react-native"; | ||
| 256 | -import { makeMutable, useAnimatedRef } from "react-native-reanimated"; | ||
| 257 | +import { Platform, StyleSheet } from "react-native"; | ||
| 258 | +import { makeMutable, runOnJS, useAnimatedRef } from "react-native-reanimated"; | ||
| 259 | import Reanimated from "react-native-reanimated"; | ||
| 260 | -import { useAnimatedStyle, useDerivedValue } from "../../reanimated"; | ||
| 261 | +import { useAnimatedReaction, useAnimatedStyle, useDerivedValue } from "../../reanimated"; | ||
| 262 | import useCombinedRef from "../hooks/useCombinedRef"; | ||
| 263 | import ScrollViewWithBottomPadding from "../ScrollViewWithBottomPadding"; | ||
| 264 | import { useChatKeyboard } from "./useChatKeyboard"; | ||
| 265 | @@ -21,9 +21,12 @@ const KeyboardChatScrollView = /*#__PURE__*/forwardRef(({ | ||
| 266 | offset = 0, | ||
| 267 | extraContentPadding = ZERO_CONTENT_PADDING, | ||
| 268 | blankSpace = ZERO_BLANK_SPACE, | ||
| 269 | + adjustedInsetCompensation = 0, | ||
| 270 | + adjustedStartInsetCompensation = 0, | ||
| 271 | applyWorkaroundForContentInsetHitTestBug = false, | ||
| 272 | onLayout: onLayoutProp, | ||
| 273 | onContentSizeChange: onContentSizeChangeProp, | ||
| 274 | + onContentInsetChange, | ||
| 275 | onEndVisible, | ||
| 276 | ...rest | ||
| 277 | }, ref) => { | ||
| 278 | @@ -47,6 +50,8 @@ const KeyboardChatScrollView = /*#__PURE__*/forwardRef(({ | ||
| 279 | offset, | ||
| 280 | blankSpace, | ||
| 281 | extraContentPadding, | ||
| 282 | + adjustedInsetCompensation, | ||
| 283 | + adjustedStartInsetCompensation, | ||
| 284 | initialContentOffsetY: (_rest$contentOffset = rest.contentOffset) === null || _rest$contentOffset === void 0 ? void 0 : _rest$contentOffset.y | ||
| 285 | }); | ||
| 286 | useExtraContentPadding({ | ||
| 287 | @@ -54,6 +59,8 @@ const KeyboardChatScrollView = /*#__PURE__*/forwardRef(({ | ||
| 288 | extraContentPadding, | ||
| 289 | keyboardPadding: padding, | ||
| 290 | blankSpace, | ||
| 291 | + adjustedInsetCompensation, | ||
| 292 | + adjustedStartInsetCompensation, | ||
| 293 | scroll, | ||
| 294 | layout, | ||
| 295 | size, | ||
| 296 | @@ -84,10 +91,26 @@ const KeyboardChatScrollView = /*#__PURE__*/forwardRef(({ | ||
| 297 | // a bug for you, please open an issue. | ||
| 298 | const totalPadding = useDerivedValue(() => Math.min(layout.value.height, Math.max(blankSpace.value, padding.value + extraContentPadding.value))); | ||
| 299 | |||
| 300 | + // iOS applies the destination contentInset and contentOffset together at | ||
| 301 | + // keyboard-animation start. Keep that native target behavior, but report | ||
| 302 | + // the presentation height so virtualized-list layout follows the keyboard. | ||
| 303 | + const reportedPadding = useDerivedValue(() => Platform.OS === "ios" ? Math.min(layout.value.height, Math.max(blankSpace.value, currentHeight.value + extraContentPadding.value)) : totalPadding.value); | ||
| 304 | + | ||
| 305 | + // Mirror the effective bottom padding (keyboard + composer + blank floor) | ||
| 306 | + // to the consumer - a virtualized list needs it in its own scroll math or | ||
| 307 | + // its end/maintain targets point at the under-the-keyboard resting offset. | ||
| 308 | + useAnimatedReaction(() => reportedPadding.value, (current, previous) => { | ||
| 309 | + if (onContentInsetChange && current !== previous) { | ||
| 310 | + runOnJS(onContentInsetChange)({ | ||
| 311 | + bottom: current | ||
| 312 | + }); | ||
| 313 | + } | ||
| 314 | + }, [onContentInsetChange]); | ||
| 315 | + | ||
| 316 | // Scroll indicator inset = keyboard + extraContentPadding (excludes blankSpace). | ||
| 317 | // Apps that render into the unsafe area can supply a negative | ||
| 318 | // scrollIndicatorInsets adjustment at the application layer. | ||
| 319 | - const indicatorPadding = useDerivedValue(() => padding.value + extraContentPadding.value); | ||
| 320 | + const indicatorPadding = useDerivedValue(() => padding.value); | ||
| 321 | const onLayout = useCallback(e => { | ||
| 322 | onLayoutInternal(e); | ||
| 323 | onLayoutProp === null || onLayoutProp === void 0 || onLayoutProp(e); | ||
| 324 | diff --git a/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js b/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js | ||
| 325 | index 295e221..2f7b87e 100644 | ||
| 326 | --- a/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js | ||
| 327 | +++ b/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js | ||
| 328 | @@ -193,7 +193,7 @@ export const clampedScrollTarget = (offsetBeforeScroll, keyboardHeight, contentH | ||
| 329 | * computeIOSContentOffset(100, 300, 1000, 800, false); // 400 | ||
| 330 | * ``` | ||
| 331 | */ | ||
| 332 | -export const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentHeight, layoutHeight, inverted, totalPaddingForMaxScroll) => { | ||
| 333 | +export const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentHeight, layoutHeight, inverted, totalPaddingForMaxScroll, startInsetCompensation = 0) => { | ||
| 334 | "worklet"; | ||
| 335 | |||
| 336 | const paddingForMax = totalPaddingForMaxScroll !== undefined ? totalPaddingForMaxScroll : keyboardHeight; | ||
| 337 | @@ -201,7 +201,8 @@ export const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentH | ||
| 338 | const maxScroll = Math.max(contentHeight - layoutHeight, 0); | ||
| 339 | return Math.max(Math.min(relativeScroll - keyboardHeight, maxScroll), -paddingForMax); | ||
| 340 | } | ||
| 341 | - const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, 0); | ||
| 342 | - return Math.min(Math.max(keyboardHeight + relativeScroll, 0), maxScroll); | ||
| 343 | + const minScroll = -startInsetCompensation; | ||
| 344 | + const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, minScroll); | ||
| 345 | + return Math.min(Math.max(keyboardHeight + relativeScroll, minScroll), maxScroll); | ||
| 346 | }; | ||
| 347 | //# sourceMappingURL=helpers.js.map | ||
| 348 | \ No newline at end of file | ||
| 349 | diff --git a/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js b/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js | ||
| 350 | index 043b973..df5a408 100644 | ||
| 351 | --- a/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js | ||
| 352 | +++ b/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js | ||
| 353 | @@ -25,7 +25,9 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 354 | freeze, | ||
| 355 | offset, | ||
| 356 | blankSpace, | ||
| 357 | - extraContentPadding | ||
| 358 | + extraContentPadding, | ||
| 359 | + adjustedInsetCompensation, | ||
| 360 | + adjustedStartInsetCompensation | ||
| 361 | } = options; | ||
| 362 | const padding = useSharedValue(0); | ||
| 363 | const currentHeight = useSharedValue(0); | ||
| 364 | @@ -52,6 +54,9 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 365 | targetKeyboardHeight.value = e.height; | ||
| 366 | } | ||
| 367 | const effective = getEffectiveHeight(e.height, targetKeyboardHeight.value, offset); | ||
| 368 | + if (e.duration === 0) { | ||
| 369 | + currentHeight.value = effective; | ||
| 370 | + } | ||
| 371 | const atEnd = isScrollAtEnd(scroll.value, layout.value.height, size.value.height, inverted); | ||
| 372 | |||
| 373 | // Scale minimum padding absorption by how much of it is visible. | ||
| 374 | @@ -60,7 +65,7 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 375 | const visiblePadding = visibleFraction * blankSpace.value; | ||
| 376 | const minimumPaddingAbsorbed = Math.max(0, visiblePadding - extraContentPadding.value); | ||
| 377 | const scrollEffective = getScrollEffective(effective, minimumPaddingAbsorbed); | ||
| 378 | - const actualTotalPadding = Math.max(blankSpace.value, effective + extraContentPadding.value); | ||
| 379 | + const actualTotalPadding = Math.max(blankSpace.value, effective + extraContentPadding.value) + adjustedInsetCompensation; | ||
| 380 | |||
| 381 | // `onInteractive` owns the native scroll offset. When its final | ||
| 382 | // keyboardWillHide event arrives, consume the marker so this | ||
| 383 | @@ -80,8 +85,8 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 384 | const maxScroll = Math.max(size.value.height - layout.value.height, 0); | ||
| 385 | contentOffsetY.value = Math.max(-actualTotalPadding, Math.min(scroll.value, maxScroll)); | ||
| 386 | } else { | ||
| 387 | - const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, 0); | ||
| 388 | - contentOffsetY.value = Math.max(0, Math.min(scroll.value, maxScroll)); | ||
| 389 | + const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, -adjustedStartInsetCompensation); | ||
| 390 | + contentOffsetY.value = Math.max(-adjustedStartInsetCompensation, Math.min(scroll.value, maxScroll)); | ||
| 391 | } | ||
| 392 | return; | ||
| 393 | } | ||
| 394 | @@ -95,8 +100,8 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 395 | const maxScroll = Math.max(size.value.height - layout.value.height, 0); | ||
| 396 | contentOffsetY.value = Math.max(-actualTotalPadding, Math.min(scroll.value, maxScroll)); | ||
| 397 | } else { | ||
| 398 | - const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, 0); | ||
| 399 | - contentOffsetY.value = Math.max(0, Math.min(scroll.value, maxScroll)); | ||
| 400 | + const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, -adjustedStartInsetCompensation); | ||
| 401 | + contentOffsetY.value = Math.max(-adjustedStartInsetCompensation, Math.min(scroll.value, maxScroll)); | ||
| 402 | } | ||
| 403 | return; | ||
| 404 | } | ||
| 405 | @@ -122,17 +127,25 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 406 | contentOffsetY.value = scroll.value; | ||
| 407 | return; | ||
| 408 | } | ||
| 409 | - contentOffsetY.value = computeIOSContentOffset(relativeScroll, scrollEffective, size.value.height, layout.value.height, inverted, actualTotalPadding); | ||
| 410 | + contentOffsetY.value = computeIOSContentOffset(relativeScroll, scrollEffective, size.value.height, layout.value.height, inverted, actualTotalPadding, adjustedStartInsetCompensation); | ||
| 411 | }, | ||
| 412 | - onInteractive: () => { | ||
| 413 | + onMove: e => { | ||
| 414 | "worklet"; | ||
| 415 | |||
| 416 | - isInteractiveDismissal.value = true; | ||
| 417 | + if (freeze.value) { | ||
| 418 | + return; | ||
| 419 | + } | ||
| 420 | + currentHeight.value = getEffectiveHeight(e.height, targetKeyboardHeight.value, offset); | ||
| 421 | }, | ||
| 422 | - onMove: () => { | ||
| 423 | + onInteractive: e => { | ||
| 424 | "worklet"; | ||
| 425 | |||
| 426 | - // iOS doesn't need per-frame updates (contentOffset handles it) | ||
| 427 | + isInteractiveDismissal.value = true; | ||
| 428 | + | ||
| 429 | + if (freeze.value) { | ||
| 430 | + return; | ||
| 431 | + } | ||
| 432 | + currentHeight.value = getEffectiveHeight(e.height, targetKeyboardHeight.value, offset); | ||
| 433 | }, | ||
| 434 | onEnd: e => { | ||
| 435 | "worklet"; | ||
| 436 | @@ -141,9 +154,10 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 437 | return; | ||
| 438 | } | ||
| 439 | const effective = getEffectiveHeight(e.height, targetKeyboardHeight.value, offset); | ||
| 440 | + currentHeight.value = effective; | ||
| 441 | padding.value = effective; | ||
| 442 | } | ||
| 443 | - }, [inverted, keyboardLiftBehavior, offset, extraContentPadding]); | ||
| 444 | + }, [inverted, keyboardLiftBehavior, offset, extraContentPadding, adjustedInsetCompensation, adjustedStartInsetCompensation]); | ||
| 445 | return { | ||
| 446 | padding, | ||
| 447 | currentHeight, | ||
| 448 | diff --git a/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js b/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js | ||
| 449 | index 5a9b91d..a11bfdb 100644 | ||
| 450 | --- a/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js | ||
| 451 | +++ b/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js | ||
| 452 | @@ -26,6 +26,8 @@ function useExtraContentPadding(options) { | ||
| 453 | extraContentPadding, | ||
| 454 | keyboardPadding, | ||
| 455 | blankSpace, | ||
| 456 | + adjustedInsetCompensation, | ||
| 457 | + adjustedStartInsetCompensation, | ||
| 458 | scroll, | ||
| 459 | layout, | ||
| 460 | size, | ||
| 461 | @@ -45,8 +47,13 @@ function useExtraContentPadding(options) { | ||
| 462 | // otherwise the native ScrollView clamps to the old range. | ||
| 463 | requestAnimationFrame(() => { | ||
| 464 | // check that view is still mounted and ref is actual | ||
| 465 | - // otherwise it may lead to a crash | ||
| 466 | - if (!scrollViewRef()) { | ||
| 467 | + // otherwise it may lead to a crash. Reanimated 4.7+ passes the | ||
| 468 | + // ref to the UI runtime as a shareable holding the node in `.value`. | ||
| 469 | + const node = | ||
| 470 | + typeof scrollViewRef === "function" | ||
| 471 | + ? scrollViewRef() | ||
| 472 | + : scrollViewRef.value; | ||
| 473 | + if (!node) { | ||
| 474 | return; | ||
| 475 | } | ||
| 476 | scrollTo(scrollViewRef, 0, target, false); | ||
| 477 | @@ -65,8 +72,8 @@ function useExtraContentPadding(options) { | ||
| 478 | } | ||
| 479 | |||
| 480 | // Compute effective delta considering blankSpace floor | ||
| 481 | - const previousTotal = Math.max(blankSpace.value, keyboardPadding.value + previous); | ||
| 482 | - const currentTotal = Math.max(blankSpace.value, keyboardPadding.value + current); | ||
| 483 | + const previousTotal = Math.max(blankSpace.value, keyboardPadding.value + previous) + adjustedInsetCompensation; | ||
| 484 | + const currentTotal = Math.max(blankSpace.value, keyboardPadding.value + current) + adjustedInsetCompensation; | ||
| 485 | const effectiveDelta = currentTotal - previousTotal; | ||
| 486 | if (effectiveDelta === 0) { | ||
| 487 | // blankSpace absorbed the change | ||
| 488 | @@ -85,14 +92,12 @@ function useExtraContentPadding(options) { | ||
| 489 | const target = Math.max(scroll.value - effectiveDelta, -currentTotal); | ||
| 490 | scrollToTarget(target); | ||
| 491 | } else { | ||
| 492 | - const maxScroll = Math.max(size.value.height - layout.value.height + currentTotal, 0); | ||
| 493 | - // Clamp at 0 as well: when the content is shorter than the viewport a | ||
| 494 | - // shrinking padding makes `effectiveDelta` negative while `scroll.value` | ||
| 495 | - // is already 0, so the target would go below the top of the content. | ||
| 496 | - const target = Math.max(Math.min(scroll.value + effectiveDelta, maxScroll), 0); | ||
| 497 | + const minScroll = -adjustedStartInsetCompensation; | ||
| 498 | + const maxScroll = Math.max(size.value.height - layout.value.height + currentTotal, minScroll); | ||
| 499 | + const target = Math.max(minScroll, Math.min(scroll.value + effectiveDelta, maxScroll)); | ||
| 500 | scrollToTarget(target); | ||
| 501 | } | ||
| 502 | - }, [inverted, keyboardLiftBehavior]); | ||
| 503 | + }, [inverted, keyboardLiftBehavior, adjustedInsetCompensation, adjustedStartInsetCompensation]); | ||
| 504 | } | ||
| 505 | export { useExtraContentPadding }; | ||
| 506 | //# sourceMappingURL=index.js.map | ||
| 507 | \ No newline at end of file | ||
| 508 | diff --git a/lib/typescript/components/KeyboardChatScrollView/types.d.ts b/lib/typescript/components/KeyboardChatScrollView/types.d.ts | ||
| 509 | index eba1514..6597d80 100644 | ||
| 510 | --- a/lib/typescript/components/KeyboardChatScrollView/types.d.ts | ||
| 511 | +++ b/lib/typescript/components/KeyboardChatScrollView/types.d.ts | ||
| 512 | @@ -88,6 +88,10 @@ export type KeyboardChatScrollViewProps = { | ||
| 513 | * Default is `undefined` (equivalent to `0` — no minimum floor). | ||
| 514 | */ | ||
| 515 | blankSpace?: SharedValue<number>; | ||
| 516 | + /** Extra bottom inset UIKit adds beyond the raw contentInset (safe area). Offset math only. */ | ||
| 517 | + adjustedInsetCompensation?: number; | ||
| 518 | + /** Leading inset UIKit adds outside raw contentInset. Offset math only. */ | ||
| 519 | + adjustedStartInsetCompensation?: number; | ||
| 520 | /** | ||
| 521 | * Fires whenever the effective content inset changes — the static `contentInset` | ||
| 522 | * prop combined with the dynamic keyboard-driven padding. | ||
| 523 | diff --git a/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/helpers.d.ts b/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/helpers.d.ts | ||
| 524 | index 2b51cc4..3acec1f 100644 | ||
| 525 | --- a/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/helpers.d.ts | ||
| 526 | +++ b/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/helpers.d.ts | ||
| 527 | @@ -129,4 +129,4 @@ export declare const clampedScrollTarget: (offsetBeforeScroll: number, keyboardH | ||
| 528 | * computeIOSContentOffset(100, 300, 1000, 800, false); // 400 | ||
| 529 | * ``` | ||
| 530 | */ | ||
| 531 | -export declare const computeIOSContentOffset: (relativeScroll: number, keyboardHeight: number, contentHeight: number, layoutHeight: number, inverted: boolean, totalPaddingForMaxScroll?: number) => number; | ||
| 532 | +export declare const computeIOSContentOffset: (relativeScroll: number, keyboardHeight: number, contentHeight: number, layoutHeight: number, inverted: boolean, totalPaddingForMaxScroll?: number, startInsetCompensation?: number) => number; | ||
| 533 | diff --git a/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts b/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts | ||
| 534 | index 8d358bc..911f3d9 100644 | ||
| 535 | --- a/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts | ||
| 536 | +++ b/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts | ||
| 537 | @@ -11,6 +11,10 @@ type UseChatKeyboardOptions = { | ||
| 538 | blankSpace: SharedValue<number>; | ||
| 539 | /** Extra content padding shared value — needed on iOS to correctly clamp contentOffset. */ | ||
| 540 | extraContentPadding: SharedValue<number>; | ||
| 541 | + /** Safe-area extra beyond raw contentInset. Offset math only. */ | ||
| 542 | + adjustedInsetCompensation: number; | ||
| 543 | + /** Leading inset UIKit adds beyond the raw contentInset. Offset math only. */ | ||
| 544 | + adjustedStartInsetCompensation: number; | ||
| 545 | }; | ||
| 546 | type UseChatKeyboardReturn = { | ||
| 547 | /** Extra scrollable space (= keyboard height). Used as contentInset on iOS, contentInsetBottom/contentInsetTop on Android. */ | ||
| 548 | diff --git a/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts b/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts | ||
| 549 | index ec73f70..45b3874 100644 | ||
| 550 | --- a/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts | ||
| 551 | +++ b/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts | ||
| 552 | @@ -8,6 +8,10 @@ type UseExtraContentPaddingOptions = { | ||
| 553 | keyboardPadding: SharedValue<number>; | ||
| 554 | /** Minimum inset floor — used to absorb keyboard and extraContentPadding changes. */ | ||
| 555 | blankSpace: SharedValue<number>; | ||
| 556 | + /** Safe-area extra beyond raw contentInset. Offset math only. */ | ||
| 557 | + adjustedInsetCompensation: number; | ||
| 558 | + /** Leading inset UIKit adds beyond raw contentInset. Offset math only. */ | ||
| 559 | + adjustedStartInsetCompensation: number; | ||
| 560 | /** Current vertical scroll offset. */ | ||
| 561 | scroll: SharedValue<number>; | ||
| 562 | /** Visible viewport dimensions. */ | ||
| 563 | diff --git a/src/components/KeyboardChatScrollView/index.tsx b/src/components/KeyboardChatScrollView/index.tsx | ||
| 564 | index 935bc71..d411dc4 100644 | ||
| 565 | --- a/src/components/KeyboardChatScrollView/index.tsx | ||
| 566 | +++ b/src/components/KeyboardChatScrollView/index.tsx | ||
| 567 | @@ -1,9 +1,13 @@ | ||
| 568 | import React, { forwardRef, useCallback, useMemo } from "react"; | ||
| 569 | -import { StyleSheet } from "react-native"; | ||
| 570 | -import { makeMutable, useAnimatedRef } from "react-native-reanimated"; | ||
| 571 | +import { Platform, StyleSheet } from "react-native"; | ||
| 572 | +import { makeMutable, runOnJS, useAnimatedRef } from "react-native-reanimated"; | ||
| 573 | import Reanimated from "react-native-reanimated"; | ||
| 574 | |||
| 575 | -import { useAnimatedStyle, useDerivedValue } from "../../reanimated"; | ||
| 576 | +import { | ||
| 577 | + useAnimatedReaction, | ||
| 578 | + useAnimatedStyle, | ||
| 579 | + useDerivedValue, | ||
| 580 | +} from "../../reanimated"; | ||
| 581 | import useCombinedRef from "../hooks/useCombinedRef"; | ||
| 582 | import ScrollViewWithBottomPadding from "../ScrollViewWithBottomPadding"; | ||
| 583 | |||
| 584 | @@ -35,9 +39,12 @@ const KeyboardChatScrollView: React.ForwardRefExoticComponent< | ||
| 585 | offset = 0, | ||
| 586 | extraContentPadding = ZERO_CONTENT_PADDING, | ||
| 587 | blankSpace = ZERO_BLANK_SPACE, | ||
| 588 | + adjustedInsetCompensation = 0, | ||
| 589 | + adjustedStartInsetCompensation = 0, | ||
| 590 | applyWorkaroundForContentInsetHitTestBug = false, | ||
| 591 | onLayout: onLayoutProp, | ||
| 592 | onContentSizeChange: onContentSizeChangeProp, | ||
| 593 | + onContentInsetChange, | ||
| 594 | onEndVisible, | ||
| 595 | ...rest | ||
| 596 | }, | ||
| 597 | @@ -64,6 +71,8 @@ const KeyboardChatScrollView: React.ForwardRefExoticComponent< | ||
| 598 | offset, | ||
| 599 | blankSpace, | ||
| 600 | extraContentPadding, | ||
| 601 | + adjustedInsetCompensation, | ||
| 602 | + adjustedStartInsetCompensation, | ||
| 603 | initialContentOffsetY: rest.contentOffset?.y, | ||
| 604 | }); | ||
| 605 | |||
| 606 | @@ -72,6 +81,8 @@ const KeyboardChatScrollView: React.ForwardRefExoticComponent< | ||
| 607 | extraContentPadding, | ||
| 608 | keyboardPadding: padding, | ||
| 609 | blankSpace, | ||
| 610 | + adjustedInsetCompensation, | ||
| 611 | + adjustedStartInsetCompensation, | ||
| 612 | scroll, | ||
| 613 | layout, | ||
| 614 | size, | ||
| 615 | @@ -109,13 +120,40 @@ const KeyboardChatScrollView: React.ForwardRefExoticComponent< | ||
| 616 | ), | ||
| 617 | ); | ||
| 618 | |||
| 619 | - // Scroll indicator inset = keyboard + extraContentPadding (excludes blankSpace). | ||
| 620 | - // Apps that render into the unsafe area can supply a negative | ||
| 621 | - // scrollIndicatorInsets adjustment at the application layer. | ||
| 622 | - const indicatorPadding = useDerivedValue( | ||
| 623 | - () => padding.value + extraContentPadding.value, | ||
| 624 | + // iOS applies the destination contentInset and contentOffset together at | ||
| 625 | + // keyboard-animation start. Keep that native target behavior, but report | ||
| 626 | + // the presentation height so virtualized-list layout follows the keyboard. | ||
| 627 | + const reportedPadding = useDerivedValue(() => | ||
| 628 | + Platform.OS === "ios" | ||
| 629 | + ? Math.min( | ||
| 630 | + layout.value.height, | ||
| 631 | + Math.max( | ||
| 632 | + blankSpace.value, | ||
| 633 | + currentHeight.value + extraContentPadding.value, | ||
| 634 | + ), | ||
| 635 | + ) | ||
| 636 | + : totalPadding.value, | ||
| 637 | ); | ||
| 638 | |||
| 639 | + // Mirror the effective bottom padding (keyboard + composer + blank floor) | ||
| 640 | + // to the consumer — a virtualized list needs it in its own scroll math or | ||
| 641 | + // its end/maintain targets point at the under-the-keyboard resting offset. | ||
| 642 | + useAnimatedReaction( | ||
| 643 | + () => reportedPadding.value, | ||
| 644 | + (current, previous) => { | ||
| 645 | + if (onContentInsetChange && current !== previous) { | ||
| 646 | + runOnJS(onContentInsetChange)({ bottom: current }); | ||
| 647 | + } | ||
| 648 | + }, | ||
| 649 | + [onContentInsetChange], | ||
| 650 | + ); | ||
| 651 | + | ||
| 652 | + // Scroll indicator inset = keyboard only (excludes extraContentPadding and | ||
| 653 | + // blankSpace): with a floating composer the indicator track should run the | ||
| 654 | + // full height of the scroll view, behind the composer, like iOS Messages. | ||
| 655 | + // The keyboard still lifts it so it never disappears under the keyboard. | ||
| 656 | + const indicatorPadding = useDerivedValue(() => padding.value); | ||
| 657 | + | ||
| 658 | const onLayout = useCallback( | ||
| 659 | (e: LayoutChangeEvent) => { | ||
| 660 | onLayoutInternal(e); | ||
| 661 | diff --git a/src/components/KeyboardChatScrollView/types.ts b/src/components/KeyboardChatScrollView/types.ts | ||
| 662 | index b7c98c8..e1012a8 100644 | ||
| 663 | --- a/src/components/KeyboardChatScrollView/types.ts | ||
| 664 | +++ b/src/components/KeyboardChatScrollView/types.ts | ||
| 665 | @@ -93,6 +93,17 @@ export type KeyboardChatScrollViewProps = { | ||
| 666 | * Default is `undefined` (equivalent to `0` — no minimum floor). | ||
| 667 | */ | ||
| 668 | blankSpace?: SharedValue<number>; | ||
| 669 | + /** | ||
| 670 | + * Extra bottom inset UIKit adds on top of the raw `contentInset` this | ||
| 671 | + * component writes (e.g. the home-indicator safe area under | ||
| 672 | + * `contentInsetAdjustmentBehavior="automatic"`). Used ONLY in scroll-offset | ||
| 673 | + * math (max scroll / end pinning) — never written into `contentInset`. | ||
| 674 | + * | ||
| 675 | + * Default is `0`. | ||
| 676 | + */ | ||
| 677 | + adjustedInsetCompensation?: number; | ||
| 678 | + /** Leading inset UIKit adds outside raw contentInset. Offset math only. */ | ||
| 679 | + adjustedStartInsetCompensation?: number; | ||
| 680 | /** | ||
| 681 | * Fires whenever the effective content inset changes — the static `contentInset` | ||
| 682 | * prop combined with the dynamic keyboard-driven padding. | ||
| 683 | diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/helpers.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/helpers.ts | ||
| 684 | index 37eacd1..3d77ab2 100644 | ||
| 685 | --- a/src/components/KeyboardChatScrollView/useChatKeyboard/helpers.ts | ||
| 686 | +++ b/src/components/KeyboardChatScrollView/useChatKeyboard/helpers.ts | ||
| 687 | @@ -245,6 +245,7 @@ export const computeIOSContentOffset = ( | ||
| 688 | layoutHeight: number, | ||
| 689 | inverted: boolean, | ||
| 690 | totalPaddingForMaxScroll?: number, | ||
| 691 | + startInsetCompensation: number = 0, | ||
| 692 | ): number => { | ||
| 693 | "worklet"; | ||
| 694 | |||
| 695 | @@ -262,7 +263,14 @@ export const computeIOSContentOffset = ( | ||
| 696 | ); | ||
| 697 | } | ||
| 698 | |||
| 699 | - const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, 0); | ||
| 700 | + const minScroll = -startInsetCompensation; | ||
| 701 | + const maxScroll = Math.max( | ||
| 702 | + contentHeight - layoutHeight + paddingForMax, | ||
| 703 | + minScroll, | ||
| 704 | + ); | ||
| 705 | |||
| 706 | - return Math.min(Math.max(keyboardHeight + relativeScroll, 0), maxScroll); | ||
| 707 | + return Math.min( | ||
| 708 | + Math.max(keyboardHeight + relativeScroll, minScroll), | ||
| 709 | + maxScroll, | ||
| 710 | + ); | ||
| 711 | }; | ||
| 712 | diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts | ||
| 713 | index 1ae9b81..208505b 100644 | ||
| 714 | --- a/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts | ||
| 715 | +++ b/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts | ||
| 716 | @@ -43,6 +43,8 @@ function useChatKeyboard( | ||
| 717 | offset, | ||
| 718 | blankSpace, | ||
| 719 | extraContentPadding, | ||
| 720 | + adjustedInsetCompensation, | ||
| 721 | + adjustedStartInsetCompensation, | ||
| 722 | } = options; | ||
| 723 | |||
| 724 | const padding = useSharedValue(0); | ||
| 725 | @@ -80,6 +82,10 @@ function useChatKeyboard( | ||
| 726 | offset, | ||
| 727 | ); | ||
| 728 | |||
| 729 | + if (e.duration === 0) { | ||
| 730 | + currentHeight.value = effective; | ||
| 731 | + } | ||
| 732 | + | ||
| 733 | const atEnd = isScrollAtEnd( | ||
| 734 | scroll.value, | ||
| 735 | layout.value.height, | ||
| 736 | @@ -105,10 +111,12 @@ function useChatKeyboard( | ||
| 737 | effective, | ||
| 738 | minimumPaddingAbsorbed, | ||
| 739 | ); | ||
| 740 | - const actualTotalPadding = Math.max( | ||
| 741 | - blankSpace.value, | ||
| 742 | - effective + extraContentPadding.value, | ||
| 743 | - ); | ||
| 744 | + // UIKit adds adjustedInsetCompensation (safe area) on top of the raw | ||
| 745 | + // inset; include it here so end/max-scroll targets match the real | ||
| 746 | + // resting offsets. | ||
| 747 | + const actualTotalPadding = | ||
| 748 | + Math.max(blankSpace.value, effective + extraContentPadding.value) + | ||
| 749 | + adjustedInsetCompensation; | ||
| 750 | |||
| 751 | // `onInteractive` owns the native scroll offset. When its final | ||
| 752 | // keyboardWillHide event arrives, consume the marker so this | ||
| 753 | @@ -142,11 +150,11 @@ function useChatKeyboard( | ||
| 754 | } else { | ||
| 755 | const maxScroll = Math.max( | ||
| 756 | size.value.height - layout.value.height + actualTotalPadding, | ||
| 757 | - 0, | ||
| 758 | + -adjustedStartInsetCompensation, | ||
| 759 | ); | ||
| 760 | |||
| 761 | contentOffsetY.value = Math.max( | ||
| 762 | - 0, | ||
| 763 | + -adjustedStartInsetCompensation, | ||
| 764 | Math.min(scroll.value, maxScroll), | ||
| 765 | ); | ||
| 766 | } | ||
| 767 | @@ -177,11 +185,11 @@ function useChatKeyboard( | ||
| 768 | } else { | ||
| 769 | const maxScroll = Math.max( | ||
| 770 | size.value.height - layout.value.height + actualTotalPadding, | ||
| 771 | - 0, | ||
| 772 | + -adjustedStartInsetCompensation, | ||
| 773 | ); | ||
| 774 | |||
| 775 | contentOffsetY.value = Math.max( | ||
| 776 | - 0, | ||
| 777 | + -adjustedStartInsetCompensation, | ||
| 778 | Math.min(scroll.value, maxScroll), | ||
| 779 | ); | ||
| 780 | } | ||
| 781 | @@ -231,17 +239,36 @@ function useChatKeyboard( | ||
| 782 | layout.value.height, | ||
| 783 | inverted, | ||
| 784 | actualTotalPadding, | ||
| 785 | + adjustedStartInsetCompensation, | ||
| 786 | ); | ||
| 787 | }, | ||
| 788 | - onInteractive: () => { | ||
| 789 | + onMove: (e) => { | ||
| 790 | "worklet"; | ||
| 791 | |||
| 792 | - isInteractiveDismissal.value = true; | ||
| 793 | + if (freeze.value) { | ||
| 794 | + return; | ||
| 795 | + } | ||
| 796 | + | ||
| 797 | + currentHeight.value = getEffectiveHeight( | ||
| 798 | + e.height, | ||
| 799 | + targetKeyboardHeight.value, | ||
| 800 | + offset, | ||
| 801 | + ); | ||
| 802 | }, | ||
| 803 | - onMove: () => { | ||
| 804 | + onInteractive: (e) => { | ||
| 805 | "worklet"; | ||
| 806 | |||
| 807 | - // iOS doesn't need per-frame updates (contentOffset handles it) | ||
| 808 | + isInteractiveDismissal.value = true; | ||
| 809 | + | ||
| 810 | + if (freeze.value) { | ||
| 811 | + return; | ||
| 812 | + } | ||
| 813 | + | ||
| 814 | + currentHeight.value = getEffectiveHeight( | ||
| 815 | + e.height, | ||
| 816 | + targetKeyboardHeight.value, | ||
| 817 | + offset, | ||
| 818 | + ); | ||
| 819 | }, | ||
| 820 | onEnd: (e) => { | ||
| 821 | "worklet"; | ||
| 822 | @@ -256,10 +283,18 @@ function useChatKeyboard( | ||
| 823 | offset, | ||
| 824 | ); | ||
| 825 | |||
| 826 | + currentHeight.value = effective; | ||
| 827 | padding.value = effective; | ||
| 828 | }, | ||
| 829 | }, | ||
| 830 | - [inverted, keyboardLiftBehavior, offset, extraContentPadding], | ||
| 831 | + [ | ||
| 832 | + inverted, | ||
| 833 | + keyboardLiftBehavior, | ||
| 834 | + offset, | ||
| 835 | + extraContentPadding, | ||
| 836 | + adjustedInsetCompensation, | ||
| 837 | + adjustedStartInsetCompensation, | ||
| 838 | + ], | ||
| 839 | ); | ||
| 840 | |||
| 841 | return { | ||
| 842 | diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts | ||
| 843 | index f89b7c8..02e0e19 100644 | ||
| 844 | --- a/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts | ||
| 845 | +++ b/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts | ||
| 846 | @@ -13,6 +13,10 @@ type UseChatKeyboardOptions = { | ||
| 847 | blankSpace: SharedValue<number>; | ||
| 848 | /** Extra content padding shared value — needed on iOS to correctly clamp contentOffset. */ | ||
| 849 | extraContentPadding: SharedValue<number>; | ||
| 850 | + /** Extra bottom inset UIKit adds beyond the raw contentInset (safe area). Offset math only. */ | ||
| 851 | + adjustedInsetCompensation: number; | ||
| 852 | + /** Leading inset UIKit adds beyond the raw contentInset. Offset math only. */ | ||
| 853 | + adjustedStartInsetCompensation: number; | ||
| 854 | }; | ||
| 855 | |||
| 856 | type UseChatKeyboardReturn = { | ||
| 857 | diff --git a/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts b/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts | ||
| 858 | index de7d6f7..631cdfb 100644 | ||
| 859 | --- a/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts | ||
| 860 | +++ b/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts | ||
| 861 | @@ -17,6 +17,10 @@ type UseExtraContentPaddingOptions = { | ||
| 862 | keyboardPadding: SharedValue<number>; | ||
| 863 | /** Minimum inset floor — used to absorb keyboard and extraContentPadding changes. */ | ||
| 864 | blankSpace: SharedValue<number>; | ||
| 865 | + /** Extra bottom inset UIKit adds beyond the raw contentInset (safe area). Offset math only. */ | ||
| 866 | + adjustedInsetCompensation: number; | ||
| 867 | + /** Leading inset UIKit adds beyond raw contentInset. Offset math only. */ | ||
| 868 | + adjustedStartInsetCompensation: number; | ||
| 869 | /** Current vertical scroll offset. */ | ||
| 870 | scroll: SharedValue<number>; | ||
| 871 | /** Visible viewport dimensions. */ | ||
| 872 | @@ -52,6 +56,8 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void { | ||
| 873 | extraContentPadding, | ||
| 874 | keyboardPadding, | ||
| 875 | blankSpace, | ||
| 876 | + adjustedInsetCompensation, | ||
| 877 | + adjustedStartInsetCompensation, | ||
| 878 | scroll, | ||
| 879 | layout, | ||
| 880 | size, | ||
| 881 | @@ -73,8 +79,13 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void { | ||
| 882 | // otherwise the native ScrollView clamps to the old range. | ||
| 883 | requestAnimationFrame(() => { | ||
| 884 | // check that view is still mounted and ref is actual | ||
| 885 | - // otherwise it may lead to a crash | ||
| 886 | - if (!scrollViewRef()) { | ||
| 887 | + // otherwise it may lead to a crash. Reanimated 4.7+ passes the | ||
| 888 | + // ref to the UI runtime as a shareable holding the node in `.value`. | ||
| 889 | + const node = | ||
| 890 | + typeof scrollViewRef === "function" | ||
| 891 | + ? scrollViewRef() | ||
| 892 | + : (scrollViewRef as unknown as { value: unknown }).value; | ||
| 893 | + if (!node) { | ||
| 894 | return; | ||
| 895 | } | ||
| 896 | scrollTo(scrollViewRef, 0, target, false); | ||
| 897 | @@ -100,14 +111,12 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void { | ||
| 898 | } | ||
| 899 | |||
| 900 | // Compute effective delta considering blankSpace floor | ||
| 901 | - const previousTotal = Math.max( | ||
| 902 | - blankSpace.value, | ||
| 903 | - keyboardPadding.value + previous, | ||
| 904 | - ); | ||
| 905 | - const currentTotal = Math.max( | ||
| 906 | - blankSpace.value, | ||
| 907 | - keyboardPadding.value + current, | ||
| 908 | - ); | ||
| 909 | + const previousTotal = | ||
| 910 | + Math.max(blankSpace.value, keyboardPadding.value + previous) + | ||
| 911 | + adjustedInsetCompensation; | ||
| 912 | + const currentTotal = | ||
| 913 | + Math.max(blankSpace.value, keyboardPadding.value + current) + | ||
| 914 | + adjustedInsetCompensation; | ||
| 915 | const effectiveDelta = currentTotal - previousTotal; | ||
| 916 | |||
| 917 | if (effectiveDelta === 0) { | ||
| 918 | @@ -140,22 +149,28 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void { | ||
| 919 | |||
| 920 | scrollToTarget(target); | ||
| 921 | } else { | ||
| 922 | + const minScroll = -adjustedStartInsetCompensation; | ||
| 923 | const maxScroll = Math.max( | ||
| 924 | size.value.height - layout.value.height + currentTotal, | ||
| 925 | - 0, | ||
| 926 | + minScroll, | ||
| 927 | ); | ||
| 928 | - // Clamp at 0 as well: when the content is shorter than the viewport a | ||
| 929 | - // shrinking padding makes `effectiveDelta` negative while `scroll.value` | ||
| 930 | - // is already 0, so the target would go below the top of the content. | ||
| 931 | + // Clamp at the top as well: when the content is shorter than the | ||
| 932 | + // viewport a shrinking padding makes `effectiveDelta` negative while | ||
| 933 | + // `scroll.value` is already at the top. | ||
| 934 | const target = Math.max( | ||
| 935 | + minScroll, | ||
| 936 | Math.min(scroll.value + effectiveDelta, maxScroll), | ||
| 937 | - 0, | ||
| 938 | ); | ||
| 939 | |||
| 940 | scrollToTarget(target); | ||
| 941 | } | ||
| 942 | }, | ||
| 943 | - [inverted, keyboardLiftBehavior], | ||
| 944 | + [ | ||
| 945 | + inverted, | ||
| 946 | + keyboardLiftBehavior, | ||
| 947 | + adjustedInsetCompensation, | ||
| 948 | + adjustedStartInsetCompensation, | ||
| 949 | + ], | ||
| 950 | ); | ||
| 951 | } | ||
| 952 | |||
pnpm-lock.yaml+6-6| ... | @@ -111,7 +111,7 @@ patchedDependencies: | ... | @@ -111,7 +111,7 @@ patchedDependencies: |
| 111 | expo-widgets@58.0.11: 94de7964c5a67ab40e6b79d9bdf4b7bfc50ae97b70722ee8e5a35105ca83386b | 111 | expo-widgets@58.0.11: 94de7964c5a67ab40e6b79d9bdf4b7bfc50ae97b70722ee8e5a35105ca83386b |
| 112 | node-pty@1.2.0-beta.15: f2fe901c61cde17986240002d05c172d5d0272d83ffaab8d0ebeb922763be414 | 112 | node-pty@1.2.0-beta.15: f2fe901c61cde17986240002d05c172d5d0272d83ffaab8d0ebeb922763be414 |
| 113 | react-native-gesture-handler@3.2.1: d518a937e3cb2df5e7d67f1ab6b265c74a8639e4572d1b032b7b747e9b1c01a9 | 113 | react-native-gesture-handler@3.2.1: d518a937e3cb2df5e7d67f1ab6b265c74a8639e4572d1b032b7b747e9b1c01a9 |
| 114 | react-native-keyboard-controller@1.22.4: c631278492f0113f42fa8bed43c7d8ed5779424dd12af0cefc55132c2ee1d84c | 114 | react-native-keyboard-controller@1.22.6: 6e448411781347cd0b87868ee279117514a399801c18f233ddca4fa3935be75d |
| 115 | react-native-nitro-markdown@0.5.8: 642b47830730acff3761fe29cf54271d18b493c0070c750097850b7ea032e00c | 115 | react-native-nitro-markdown@0.5.8: 642b47830730acff3761fe29cf54271d18b493c0070c750097850b7ea032e00c |
| 116 | react-native-nitro-modules@0.35.9: daf4a639ce3f951af630a3711e4082d58e3a11e5a79f45efb0b59925327ebde2 | 116 | react-native-nitro-modules@0.35.9: daf4a639ce3f951af630a3711e4082d58e3a11e5a79f45efb0b59925327ebde2 |
| 117 | react-native-screens@4.28.0: cc1301436ee62c39860acc9ee1a1e60b7d47e6abd211c42effa62c056d165483 | 117 | react-native-screens@4.28.0: cc1301436ee62c39860acc9ee1a1e60b7d47e6abd211c42effa62c056d165483 |
| ... | @@ -448,8 +448,8 @@ importers: | ... | @@ -448,8 +448,8 @@ importers: |
| 448 | specifier: ^0.2.2 | 448 | specifier: ^0.2.2 |
| 449 | version: 0.2.2(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) | 449 | version: 0.2.2(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) |
| 450 | react-native-keyboard-controller: | 450 | react-native-keyboard-controller: |
| 451 | specifier: 1.22.4 | 451 | specifier: 1.22.6 |
| 452 | version: 1.22.4(patch_hash=c631278492f0113f42fa8bed43c7d8ed5779424dd12af0cefc55132c2ee1d84c)(react-native-reanimated@4.7.0(react-native-worklets@0.13.0(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0))(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0))(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) | 452 | version: 1.22.6(patch_hash=6e448411781347cd0b87868ee279117514a399801c18f233ddca4fa3935be75d)(react-native-reanimated@4.7.0(react-native-worklets@0.13.0(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0))(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0))(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) |
| 453 | react-native-nitro-markdown: | 453 | react-native-nitro-markdown: |
| 454 | specifier: ^0.5.0 | 454 | specifier: ^0.5.0 |
| 455 | version: 0.5.8(patch_hash=642b47830730acff3761fe29cf54271d18b493c0070c750097850b7ea032e00c)(react-native-nitro-modules@0.35.9(patch_hash=daf4a639ce3f951af630a3711e4082d58e3a11e5a79f45efb0b59925327ebde2)(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0))(react-native-svg@15.15.5(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0))(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) | 455 | version: 0.5.8(patch_hash=642b47830730acff3761fe29cf54271d18b493c0070c750097850b7ea032e00c)(react-native-nitro-modules@0.35.9(patch_hash=daf4a639ce3f951af630a3711e4082d58e3a11e5a79f45efb0b59925327ebde2)(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0))(react-native-svg@15.15.5(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0))(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) |
| ... | @@ -9561,8 +9561,8 @@ packages: | ... | @@ -9561,8 +9561,8 @@ packages: |
| 9561 | react: '*' | 9561 | react: '*' |
| 9562 | react-native: '*' | 9562 | react-native: '*' |
| 9563 | 9563 | ||
| 9564 | react-native-keyboard-controller@1.22.4: | 9564 | react-native-keyboard-controller@1.22.6: |
| 9565 | resolution: {integrity: sha512-2by3oJBQqKH9UAydlBeiEpivxIyTbt345bmverLym7vbEp4NXckWQLF0STnVuXYccRak5/sd3wMYDJPhv/9+3g==} | 9565 | resolution: {integrity: sha512-gwJAI3Bhbqs/Ja3uaeBz7EFQSFcem8q4wsXkrC9sy1CL14gjWhBICLu2Ecn7HmezpIu33uhLbirLGyqSawxRtg==} |
| 9566 | peerDependencies: | 9566 | peerDependencies: |
| 9567 | react: '*' | 9567 | react: '*' |
| 9568 | react-native: '*' | 9568 | react-native: '*' |
| ... | @@ -20049,7 +20049,7 @@ snapshots: | ... | @@ -20049,7 +20049,7 @@ snapshots: |
| 20049 | react: 19.3.0 | 20049 | react: 19.3.0 |
| 20050 | react-native: 0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6) | 20050 | react-native: 0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6) |
| 20051 | 20051 | ||
| 20052 | react-native-keyboard-controller@1.22.4(patch_hash=c631278492f0113f42fa8bed43c7d8ed5779424dd12af0cefc55132c2ee1d84c)(react-native-reanimated@4.7.0(react-native-worklets@0.13.0(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0))(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0))(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0): | 20052 | react-native-keyboard-controller@1.22.6(patch_hash=6e448411781347cd0b87868ee279117514a399801c18f233ddca4fa3935be75d)(react-native-reanimated@4.7.0(react-native-worklets@0.13.0(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0))(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0))(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0): |
| 20053 | dependencies: | 20053 | dependencies: |
| 20054 | react: 19.3.0 | 20054 | react: 19.3.0 |
| 20055 | react-native: 0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6) | 20055 | react-native: 0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6) |
pnpm-workspace.yaml+2-2| ... | @@ -143,7 +143,7 @@ minimumReleaseAgeExclude: | ... | @@ -143,7 +143,7 @@ minimumReleaseAgeExclude: |
| 143 | - "react@19.3.0" | 143 | - "react@19.3.0" |
| 144 | - "react-dom@19.3.0" | 144 | - "react-dom@19.3.0" |
| 145 | - "react-native@0.88.0-rc.3" | 145 | - "react-native@0.88.0-rc.3" |
| 146 | - "react-native-keyboard-controller@1.22.4" | 146 | - "react-native-keyboard-controller@1.22.6" |
| 147 | - "react-native-worklets@0.13.0" | 147 | - "react-native-worklets@0.13.0" |
| 148 | - "react-native-reanimated@4.7.0" | 148 | - "react-native-reanimated@4.7.0" |
| 149 | - "react-native-screens@4.28.0" | 149 | - "react-native-screens@4.28.0" |
| ... | @@ -260,7 +260,7 @@ patchedDependencies: | ... | @@ -260,7 +260,7 @@ patchedDependencies: |
| 260 | # Link http(s) URLs with a port or a single-label host. md4c otherwise requires a dotted host and stops at ':'. | 260 | # Link http(s) URLs with a port or a single-label host. md4c otherwise requires a dotted host and stops at ':'. |
| 261 | react-native-nitro-markdown@0.5.8: patches/react-native-nitro-markdown@0.5.8.patch | 261 | react-native-nitro-markdown@0.5.8: patches/react-native-nitro-markdown@0.5.8.patch |
| 262 | react-native-gesture-handler@3.2.1: patches/react-native-gesture-handler@3.2.1.patch | 262 | react-native-gesture-handler@3.2.1: patches/react-native-gesture-handler@3.2.1.patch |
| 263 | react-native-keyboard-controller@1.22.4: patches/react-native-keyboard-controller@1.22.4.patch | 263 | react-native-keyboard-controller@1.22.6: patches/react-native-keyboard-controller@1.22.6.patch |
| 264 | react-native-nitro-modules@0.35.9: patches/react-native-nitro-modules@0.35.9.patch | 264 | react-native-nitro-modules@0.35.9: patches/react-native-nitro-modules@0.35.9.patch |
| 265 | react-native-screens@4.28.0: patches/react-native-screens@4.28.0.patch | 265 | react-native-screens@4.28.0: patches/react-native-screens@4.28.0.patch |
| 266 | uniwind@1.11.0: patches/uniwind@1.11.0.patch | 266 | uniwind@1.11.0: patches/uniwind@1.11.0.patch |