authorgravatar for znoraka@gmail.comNoé <znoraka@gmail.com> 2026-10-03 16:34:13+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-03 14:34:13+00:00
log1945ce82c0d66d2b8ffbe867e688c0ecbf530e69
treec9a7903548dd6dd16cee69571dd347d9e681a8f4
parentf391794a35c604d57e166a3ab48d56fc6e4e469a
signature Signed by PGP key B5690EEEBB952194

fix(mobile): iOS threads no longer jump to the top (#14808)


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 @@
1diff --git a/lib/commonjs/components/KeyboardChatScrollView/index.js b/lib/commonjs/components/KeyboardChatScrollView/index.js
2index 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);
65diff --git a/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js b/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js
66index 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
91diff --git a/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js b/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js
92index 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,
190diff --git a/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js b/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js
191index 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
246diff --git a/lib/module/components/KeyboardChatScrollView/index.js b/lib/module/components/KeyboardChatScrollView/index.js
247index 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);
320diff --git a/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js b/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js
321index 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
345diff --git a/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js b/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js
346index 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,
444diff --git a/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js b/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js
445index 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
501diff --git a/lib/typescript/components/KeyboardChatScrollView/types.d.ts b/lib/typescript/components/KeyboardChatScrollView/types.d.ts
502index 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.
516diff --git a/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/helpers.d.ts b/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/helpers.d.ts
517index 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;
526diff --git a/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts b/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts
527index 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. */
541diff --git a/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts b/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts
542index 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. */
556diff --git a/src/components/KeyboardChatScrollView/index.tsx b/src/components/KeyboardChatScrollView/index.tsx
557index 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);
648diff --git a/src/components/KeyboardChatScrollView/types.ts b/src/components/KeyboardChatScrollView/types.ts
649index 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.
670diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/helpers.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/helpers.ts
671index 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 };
699diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts
700index 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 {
829diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts
830index 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 = {
844diff --git a/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts b/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts
845index 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 @@
1diff --git a/lib/commonjs/components/KeyboardChatScrollView/index.js b/lib/commonjs/components/KeyboardChatScrollView/index.js
2index 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);
64diff --git a/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js b/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js
65index 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
90diff --git a/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js b/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js
91index 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,
189diff --git a/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js b/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js
190index 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
248diff --git a/lib/module/components/KeyboardChatScrollView/index.js b/lib/module/components/KeyboardChatScrollView/index.js
249index 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);
324diff --git a/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js b/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js
325index 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
349diff --git a/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js b/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js
350index 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,
448diff --git a/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js b/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js
449index 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
508diff --git a/lib/typescript/components/KeyboardChatScrollView/types.d.ts b/lib/typescript/components/KeyboardChatScrollView/types.d.ts
509index 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.
523diff --git a/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/helpers.d.ts b/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/helpers.d.ts
524index 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;
533diff --git a/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts b/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts
534index 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. */
548diff --git a/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts b/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts
549index 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. */
563diff --git a/src/components/KeyboardChatScrollView/index.tsx b/src/components/KeyboardChatScrollView/index.tsx
564index 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);
661diff --git a/src/components/KeyboardChatScrollView/types.ts b/src/components/KeyboardChatScrollView/types.ts
662index 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.
683diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/helpers.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/helpers.ts
684index 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 };
712diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts
713index 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 {
842diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts
843index 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 = {
857diff --git a/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts b/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts
858index 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: 94de7964c5a67ab40e6b79d9bdf4b7bfc50ae97b70722ee8e5a35105ca83386b111 expo-widgets@58.0.11: 94de7964c5a67ab40e6b79d9bdf4b7bfc50ae97b70722ee8e5a35105ca83386b
112 node-pty@1.2.0-beta.15: f2fe901c61cde17986240002d05c172d5d0272d83ffaab8d0ebeb922763be414112 node-pty@1.2.0-beta.15: f2fe901c61cde17986240002d05c172d5d0272d83ffaab8d0ebeb922763be414
113 react-native-gesture-handler@3.2.1: d518a937e3cb2df5e7d67f1ab6b265c74a8639e4572d1b032b7b747e9b1c01a9113 react-native-gesture-handler@3.2.1: d518a937e3cb2df5e7d67f1ab6b265c74a8639e4572d1b032b7b747e9b1c01a9
114 react-native-keyboard-controller@1.22.4: c631278492f0113f42fa8bed43c7d8ed5779424dd12af0cefc55132c2ee1d84c114 react-native-keyboard-controller@1.22.6: 6e448411781347cd0b87868ee279117514a399801c18f233ddca4fa3935be75d
115 react-native-nitro-markdown@0.5.8: 642b47830730acff3761fe29cf54271d18b493c0070c750097850b7ea032e00c115 react-native-nitro-markdown@0.5.8: 642b47830730acff3761fe29cf54271d18b493c0070c750097850b7ea032e00c
116 react-native-nitro-modules@0.35.9: daf4a639ce3f951af630a3711e4082d58e3a11e5a79f45efb0b59925327ebde2116 react-native-nitro-modules@0.35.9: daf4a639ce3f951af630a3711e4082d58e3a11e5a79f45efb0b59925327ebde2
117 react-native-screens@4.28.0: cc1301436ee62c39860acc9ee1a1e60b7d47e6abd211c42effa62c056d165483117 react-native-screens@4.28.0: cc1301436ee62c39860acc9ee1a1e60b7d47e6abd211c42effa62c056d165483
...@@ -448,8 +448,8 @@ importers:...@@ -448,8 +448,8 @@ importers:
448 specifier: ^0.2.2448 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.4451 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.0454 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: '*'
95639563
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.020049 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)
2005120051
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.020054 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.patch261 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.patch262 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.patch263 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.patch264 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.patch265 react-native-screens@4.28.0: patches/react-native-screens@4.28.0.patch
266 uniwind@1.11.0: patches/uniwind@1.11.0.patch266 uniwind@1.11.0: patches/uniwind@1.11.0.patch