react-native-keyboard-controller 1.21.5 → 1.21.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/android/src/main/java/com/reactnativekeyboardcontroller/modules/KeyboardControllerModuleImpl.kt +11 -1
  2. package/android/src/main/java/com/reactnativekeyboardcontroller/views/ClippingScrollViewDecoratorView.kt +1 -1
  3. package/lib/commonjs/components/KeyboardChatScrollView/index.js +12 -2
  4. package/lib/commonjs/components/KeyboardChatScrollView/index.js.map +1 -1
  5. package/lib/commonjs/components/KeyboardChatScrollView/types.js.map +1 -1
  6. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js +3 -3
  7. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js.map +1 -1
  8. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.js +4 -4
  9. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.js.map +1 -1
  10. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/types.js.map +1 -1
  11. package/lib/commonjs/components/KeyboardChatScrollView/useEndVisible.js +40 -0
  12. package/lib/commonjs/components/KeyboardChatScrollView/useEndVisible.js.map +1 -0
  13. package/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js +2 -2
  14. package/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js.map +1 -1
  15. package/lib/commonjs/components/KeyboardStickyView/index.js +10 -14
  16. package/lib/commonjs/components/KeyboardStickyView/index.js.map +1 -1
  17. package/lib/commonjs/components/KeyboardToolbar/index.js +12 -3
  18. package/lib/commonjs/components/KeyboardToolbar/index.js.map +1 -1
  19. package/lib/commonjs/components/ScrollViewWithBottomPadding/index.js +35 -23
  20. package/lib/commonjs/components/ScrollViewWithBottomPadding/index.js.map +1 -1
  21. package/lib/commonjs/hooks/useKeyboardState/index.js +1 -7
  22. package/lib/commonjs/hooks/useKeyboardState/index.js.map +1 -1
  23. package/lib/commonjs/module.js +1 -1
  24. package/lib/commonjs/module.js.map +1 -1
  25. package/lib/module/components/KeyboardChatScrollView/index.js +12 -2
  26. package/lib/module/components/KeyboardChatScrollView/index.js.map +1 -1
  27. package/lib/module/components/KeyboardChatScrollView/types.js.map +1 -1
  28. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js +3 -3
  29. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js.map +1 -1
  30. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.js +4 -4
  31. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.js.map +1 -1
  32. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/types.js.map +1 -1
  33. package/lib/module/components/KeyboardChatScrollView/useEndVisible.js +33 -0
  34. package/lib/module/components/KeyboardChatScrollView/useEndVisible.js.map +1 -0
  35. package/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js +2 -2
  36. package/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js.map +1 -1
  37. package/lib/module/components/KeyboardStickyView/index.js +11 -15
  38. package/lib/module/components/KeyboardStickyView/index.js.map +1 -1
  39. package/lib/module/components/KeyboardToolbar/index.js +12 -3
  40. package/lib/module/components/KeyboardToolbar/index.js.map +1 -1
  41. package/lib/module/components/ScrollViewWithBottomPadding/index.js +37 -25
  42. package/lib/module/components/ScrollViewWithBottomPadding/index.js.map +1 -1
  43. package/lib/module/hooks/useKeyboardState/index.js +1 -7
  44. package/lib/module/hooks/useKeyboardState/index.js.map +1 -1
  45. package/lib/module/module.js +1 -1
  46. package/lib/module/module.js.map +1 -1
  47. package/lib/typescript/components/KeyboardChatScrollView/index.d.ts +3 -1
  48. package/lib/typescript/components/KeyboardChatScrollView/types.d.ts +26 -2
  49. package/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts +1 -1
  50. package/lib/typescript/components/KeyboardChatScrollView/useEndVisible.d.ts +17 -0
  51. package/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts +1 -1
  52. package/lib/typescript/components/ScrollViewWithBottomPadding/index.d.ts +12 -0
  53. package/package.json +1 -1
  54. package/src/components/KeyboardChatScrollView/index.tsx +15 -2
  55. package/src/components/KeyboardChatScrollView/types.ts +29 -2
  56. package/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts +3 -3
  57. package/src/components/KeyboardChatScrollView/useChatKeyboard/index.ts +4 -4
  58. package/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts +1 -1
  59. package/src/components/KeyboardChatScrollView/useEndVisible.ts +66 -0
  60. package/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts +3 -3
  61. package/src/components/KeyboardStickyView/index.tsx +18 -19
  62. package/src/components/KeyboardToolbar/index.tsx +13 -4
  63. package/src/components/ScrollViewWithBottomPadding/index.tsx +67 -28
  64. package/src/hooks/useKeyboardState/index.ts +1 -8
  65. package/src/module.ts +1 -1
@@ -12,7 +12,7 @@ KeyboardEvents.addListener("keyboardDidHide", e => {
12
12
  isClosed = true;
13
13
  lastState = e;
14
14
  });
15
- KeyboardEvents.addListener("keyboardDidShow", e => {
15
+ KeyboardEvents.addListener("keyboardWillShow", e => {
16
16
  isClosed = false;
17
17
  lastState = e;
18
18
  });
@@ -1 +1 @@
1
- {"version":3,"names":["KeyboardControllerNative","KeyboardEvents","isClosed","lastState","height","duration","timestamp","Date","getTime","target","type","appearance","addListener","e","dismiss","options","keepFocus","animated","Promise","resolve","subscription","undefined","remove","isVisible","state","KeyboardController","setDefaultMode","setInputMode","setFocusTo","preload"],"sources":["module.ts"],"sourcesContent":["import { KeyboardControllerNative, KeyboardEvents } from \"./bindings\";\n\nimport type {\n DismissOptions,\n KeyboardControllerModule,\n KeyboardEventData,\n} from \"./types\";\n\nlet isClosed = true;\nlet lastState: KeyboardEventData = {\n height: 0,\n duration: 0,\n timestamp: new Date().getTime(),\n target: -1,\n type: \"default\",\n appearance: \"light\",\n};\n\nKeyboardEvents.addListener(\"keyboardDidHide\", (e) => {\n isClosed = true;\n lastState = e;\n});\n\nKeyboardEvents.addListener(\"keyboardDidShow\", (e) => {\n isClosed = false;\n lastState = e;\n});\n\nconst dismiss = async (options?: Partial<DismissOptions>): Promise<void> => {\n const keepFocus = options?.keepFocus ?? false;\n const animated = options?.animated ?? true;\n\n return new Promise((resolve) => {\n if (isClosed) {\n resolve();\n\n return;\n }\n\n const subscription = KeyboardEvents.addListener(\"keyboardDidHide\", () => {\n resolve(undefined);\n subscription.remove();\n });\n\n KeyboardControllerNative.dismiss(keepFocus, animated);\n });\n};\nconst isVisible = () => !isClosed;\nconst state = () => lastState;\n\n/**\n * KeyboardController module. Helps to perform imperative actions/checks with keyboard.\n */\nexport const KeyboardController: KeyboardControllerModule = {\n setDefaultMode: KeyboardControllerNative.setDefaultMode,\n setInputMode: KeyboardControllerNative.setInputMode,\n setFocusTo: KeyboardControllerNative.setFocusTo,\n preload: KeyboardControllerNative.preload,\n dismiss,\n isVisible,\n state,\n};\n"],"mappings":"AAAA,SAASA,wBAAwB,EAAEC,cAAc,QAAQ,YAAY;AAQrE,IAAIC,QAAQ,GAAG,IAAI;AACnB,IAAIC,SAA4B,GAAG;EACjCC,MAAM,EAAE,CAAC;EACTC,QAAQ,EAAE,CAAC;EACXC,SAAS,EAAE,IAAIC,IAAI,CAAC,CAAC,CAACC,OAAO,CAAC,CAAC;EAC/BC,MAAM,EAAE,CAAC,CAAC;EACVC,IAAI,EAAE,SAAS;EACfC,UAAU,EAAE;AACd,CAAC;AAEDV,cAAc,CAACW,WAAW,CAAC,iBAAiB,EAAGC,CAAC,IAAK;EACnDX,QAAQ,GAAG,IAAI;EACfC,SAAS,GAAGU,CAAC;AACf,CAAC,CAAC;AAEFZ,cAAc,CAACW,WAAW,CAAC,iBAAiB,EAAGC,CAAC,IAAK;EACnDX,QAAQ,GAAG,KAAK;EAChBC,SAAS,GAAGU,CAAC;AACf,CAAC,CAAC;AAEF,MAAMC,OAAO,GAAG,MAAOC,OAAiC,IAAoB;EAC1E,MAAMC,SAAS,GAAG,CAAAD,OAAO,aAAPA,OAAO,uBAAPA,OAAO,CAAEC,SAAS,KAAI,KAAK;EAC7C,MAAMC,QAAQ,GAAG,CAAAF,OAAO,aAAPA,OAAO,uBAAPA,OAAO,CAAEE,QAAQ,KAAI,IAAI;EAE1C,OAAO,IAAIC,OAAO,CAAEC,OAAO,IAAK;IAC9B,IAAIjB,QAAQ,EAAE;MACZiB,OAAO,CAAC,CAAC;MAET;IACF;IAEA,MAAMC,YAAY,GAAGnB,cAAc,CAACW,WAAW,CAAC,iBAAiB,EAAE,MAAM;MACvEO,OAAO,CAACE,SAAS,CAAC;MAClBD,YAAY,CAACE,MAAM,CAAC,CAAC;IACvB,CAAC,CAAC;IAEFtB,wBAAwB,CAACc,OAAO,CAACE,SAAS,EAAEC,QAAQ,CAAC;EACvD,CAAC,CAAC;AACJ,CAAC;AACD,MAAMM,SAAS,GAAGA,CAAA,KAAM,CAACrB,QAAQ;AACjC,MAAMsB,KAAK,GAAGA,CAAA,KAAMrB,SAAS;;AAE7B;AACA;AACA;AACA,OAAO,MAAMsB,kBAA4C,GAAG;EAC1DC,cAAc,EAAE1B,wBAAwB,CAAC0B,cAAc;EACvDC,YAAY,EAAE3B,wBAAwB,CAAC2B,YAAY;EACnDC,UAAU,EAAE5B,wBAAwB,CAAC4B,UAAU;EAC/CC,OAAO,EAAE7B,wBAAwB,CAAC6B,OAAO;EACzCf,OAAO;EACPS,SAAS;EACTC;AACF,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["KeyboardControllerNative","KeyboardEvents","isClosed","lastState","height","duration","timestamp","Date","getTime","target","type","appearance","addListener","e","dismiss","options","keepFocus","animated","Promise","resolve","subscription","undefined","remove","isVisible","state","KeyboardController","setDefaultMode","setInputMode","setFocusTo","preload"],"sources":["module.ts"],"sourcesContent":["import { KeyboardControllerNative, KeyboardEvents } from \"./bindings\";\n\nimport type {\n DismissOptions,\n KeyboardControllerModule,\n KeyboardEventData,\n} from \"./types\";\n\nlet isClosed = true;\nlet lastState: KeyboardEventData = {\n height: 0,\n duration: 0,\n timestamp: new Date().getTime(),\n target: -1,\n type: \"default\",\n appearance: \"light\",\n};\n\nKeyboardEvents.addListener(\"keyboardDidHide\", (e) => {\n isClosed = true;\n lastState = e;\n});\n\nKeyboardEvents.addListener(\"keyboardWillShow\", (e) => {\n isClosed = false;\n lastState = e;\n});\n\nconst dismiss = async (options?: Partial<DismissOptions>): Promise<void> => {\n const keepFocus = options?.keepFocus ?? false;\n const animated = options?.animated ?? true;\n\n return new Promise((resolve) => {\n if (isClosed) {\n resolve();\n\n return;\n }\n\n const subscription = KeyboardEvents.addListener(\"keyboardDidHide\", () => {\n resolve(undefined);\n subscription.remove();\n });\n\n KeyboardControllerNative.dismiss(keepFocus, animated);\n });\n};\nconst isVisible = () => !isClosed;\nconst state = () => lastState;\n\n/**\n * KeyboardController module. Helps to perform imperative actions/checks with keyboard.\n */\nexport const KeyboardController: KeyboardControllerModule = {\n setDefaultMode: KeyboardControllerNative.setDefaultMode,\n setInputMode: KeyboardControllerNative.setInputMode,\n setFocusTo: KeyboardControllerNative.setFocusTo,\n preload: KeyboardControllerNative.preload,\n dismiss,\n isVisible,\n state,\n};\n"],"mappings":"AAAA,SAASA,wBAAwB,EAAEC,cAAc,QAAQ,YAAY;AAQrE,IAAIC,QAAQ,GAAG,IAAI;AACnB,IAAIC,SAA4B,GAAG;EACjCC,MAAM,EAAE,CAAC;EACTC,QAAQ,EAAE,CAAC;EACXC,SAAS,EAAE,IAAIC,IAAI,CAAC,CAAC,CAACC,OAAO,CAAC,CAAC;EAC/BC,MAAM,EAAE,CAAC,CAAC;EACVC,IAAI,EAAE,SAAS;EACfC,UAAU,EAAE;AACd,CAAC;AAEDV,cAAc,CAACW,WAAW,CAAC,iBAAiB,EAAGC,CAAC,IAAK;EACnDX,QAAQ,GAAG,IAAI;EACfC,SAAS,GAAGU,CAAC;AACf,CAAC,CAAC;AAEFZ,cAAc,CAACW,WAAW,CAAC,kBAAkB,EAAGC,CAAC,IAAK;EACpDX,QAAQ,GAAG,KAAK;EAChBC,SAAS,GAAGU,CAAC;AACf,CAAC,CAAC;AAEF,MAAMC,OAAO,GAAG,MAAOC,OAAiC,IAAoB;EAC1E,MAAMC,SAAS,GAAG,CAAAD,OAAO,aAAPA,OAAO,uBAAPA,OAAO,CAAEC,SAAS,KAAI,KAAK;EAC7C,MAAMC,QAAQ,GAAG,CAAAF,OAAO,aAAPA,OAAO,uBAAPA,OAAO,CAAEE,QAAQ,KAAI,IAAI;EAE1C,OAAO,IAAIC,OAAO,CAAEC,OAAO,IAAK;IAC9B,IAAIjB,QAAQ,EAAE;MACZiB,OAAO,CAAC,CAAC;MAET;IACF;IAEA,MAAMC,YAAY,GAAGnB,cAAc,CAACW,WAAW,CAAC,iBAAiB,EAAE,MAAM;MACvEO,OAAO,CAACE,SAAS,CAAC;MAClBD,YAAY,CAACE,MAAM,CAAC,CAAC;IACvB,CAAC,CAAC;IAEFtB,wBAAwB,CAACc,OAAO,CAACE,SAAS,EAAEC,QAAQ,CAAC;EACvD,CAAC,CAAC;AACJ,CAAC;AACD,MAAMM,SAAS,GAAGA,CAAA,KAAM,CAACrB,QAAQ;AACjC,MAAMsB,KAAK,GAAGA,CAAA,KAAMrB,SAAS;;AAE7B;AACA;AACA;AACA,OAAO,MAAMsB,kBAA4C,GAAG;EAC1DC,cAAc,EAAE1B,wBAAwB,CAAC0B,cAAc;EACvDC,YAAY,EAAE3B,wBAAwB,CAAC2B,YAAY;EACnDC,UAAU,EAAE5B,wBAAwB,CAAC4B,UAAU;EAC/CC,OAAO,EAAE7B,wBAAwB,CAAC6B,OAAO;EACzCf,OAAO;EACPS,SAAS;EACTC;AACF,CAAC","ignoreList":[]}
@@ -5,10 +5,12 @@ declare const KeyboardChatScrollView: React.ForwardRefExoticComponent<{
5
5
  inverted?: boolean;
6
6
  offset?: number;
7
7
  keyboardLiftBehavior?: import("./hooks").KeyboardLiftBehavior;
8
- freeze?: boolean;
8
+ freeze?: boolean | import("react-native-reanimated").SharedValue<boolean>;
9
9
  extraContentPadding?: import("react-native-reanimated").SharedValue<number>;
10
10
  applyWorkaroundForContentInsetHitTestBug?: boolean;
11
11
  blankSpace?: import("react-native-reanimated").SharedValue<number>;
12
+ onContentInsetChange?: (insets: import("../ScrollViewWithBottomPadding").ScrollViewContentInsets) => void;
13
+ onEndVisible?: (visible: boolean) => void;
12
14
  } & import("react-native").ScrollViewProps & {
13
15
  children?: React.ReactNode | undefined;
14
16
  } & React.RefAttributes<Reanimated.ScrollView>>;
@@ -1,4 +1,4 @@
1
- import type { AnimatedScrollViewComponent } from "../ScrollViewWithBottomPadding";
1
+ import type { AnimatedScrollViewComponent, ScrollViewContentInsets } from "../ScrollViewWithBottomPadding";
2
2
  import type { KeyboardLiftBehavior } from "./useChatKeyboard/types";
3
3
  import type { ScrollViewProps } from "react-native";
4
4
  import type { SharedValue } from "react-native-reanimated";
@@ -45,7 +45,7 @@ export type KeyboardChatScrollViewProps = {
45
45
  *
46
46
  * Default is `false`.
47
47
  */
48
- freeze?: boolean;
48
+ freeze?: boolean | SharedValue<boolean>;
49
49
  /**
50
50
  * A shared value representing additional padding from external elements
51
51
  * (e.g., a growing multiline `TextInput` in a `KeyboardStickyView`).
@@ -86,4 +86,28 @@ export type KeyboardChatScrollViewProps = {
86
86
  * Default is `undefined` (equivalent to `0` — no minimum floor).
87
87
  */
88
88
  blankSpace?: SharedValue<number>;
89
+ /**
90
+ * Fires whenever the effective content inset changes — the static `contentInset`
91
+ * prop combined with the dynamic keyboard-driven padding.
92
+ *
93
+ * Useful on Android, where the synthetic content inset is not reflected in the native
94
+ * `onScroll` event payload. Consumers such as virtualized lists computing their own
95
+ * `scrollToEnd` target can use this to track the current inset alongside scroll offsets.
96
+ */
97
+ onContentInsetChange?: (insets: ScrollViewContentInsets) => void;
98
+ /**
99
+ * Fires whenever the visibility of the content "end" changes — both when the user
100
+ * arrives at the end and when they leave it. The boolean parameter reflects the new state.
101
+ *
102
+ * For non-inverted lists, the "end" is the bottom of the content. For inverted lists
103
+ * (`inverted={true}`), the "end" is the top of the scroll view, where the latest messages
104
+ * are rendered. The same internal detection drives `keyboardLiftBehavior="whenAtEnd"`.
105
+ *
106
+ * The callback can be either a plain JS function or a Reanimated worklet — the type is
107
+ * detected automatically. Worklets run on the UI thread; plain functions are dispatched
108
+ * via `runOnJS`.
109
+ *
110
+ * Fires once on mount with the initial state (after the scroll view has been measured).
111
+ */
112
+ onEndVisible?: (visible: boolean) => void;
89
113
  } & ScrollViewProps;
@@ -4,7 +4,7 @@ type KeyboardLiftBehavior = "always" | "whenAtEnd" | "persistent" | "never";
4
4
  type UseChatKeyboardOptions = {
5
5
  inverted: boolean;
6
6
  keyboardLiftBehavior: KeyboardLiftBehavior;
7
- freeze: boolean;
7
+ freeze: SharedValue<boolean>;
8
8
  offset: number;
9
9
  blankSpace: SharedValue<number>;
10
10
  /** Extra content padding shared value — needed on iOS to correctly clamp contentOffset. */
@@ -0,0 +1,17 @@
1
+ import type { SharedValue } from "react-native-reanimated";
2
+ type EndVisibleCallback = (visible: boolean) => void;
3
+ type Options = {
4
+ scroll: SharedValue<number>;
5
+ layout: SharedValue<{
6
+ width: number;
7
+ height: number;
8
+ }>;
9
+ size: SharedValue<{
10
+ width: number;
11
+ height: number;
12
+ }>;
13
+ inverted: boolean;
14
+ onEndVisible?: EndVisibleCallback;
15
+ };
16
+ export declare const useEndVisible: ({ scroll, layout, size, inverted, onEndVisible, }: Options) => void;
17
+ export {};
@@ -24,7 +24,7 @@ type UseExtraContentPaddingOptions = {
24
24
  contentOffsetY?: SharedValue<number>;
25
25
  inverted: boolean;
26
26
  keyboardLiftBehavior: KeyboardLiftBehavior;
27
- freeze: boolean;
27
+ freeze: SharedValue<boolean>;
28
28
  };
29
29
  /**
30
30
  * Hook that reacts to `extraContentPadding` changes and conditionally
@@ -4,6 +4,12 @@ import type { ScrollViewProps } from "react-native";
4
4
  import type { SharedValue } from "react-native-reanimated";
5
5
  type AnimatedScrollViewProps = React.ComponentProps<typeof Reanimated.ScrollView>;
6
6
  export type AnimatedScrollViewComponent = React.ForwardRefExoticComponent<AnimatedScrollViewProps & React.RefAttributes<Reanimated.ScrollView>>;
7
+ export type ScrollViewContentInsets = {
8
+ top: number;
9
+ bottom: number;
10
+ left: number;
11
+ right: number;
12
+ };
7
13
  declare const ScrollViewWithBottomPadding: React.ForwardRefExoticComponent<{
8
14
  ScrollViewComponent: AnimatedScrollViewComponent;
9
15
  children?: React.ReactNode;
@@ -14,5 +20,11 @@ declare const ScrollViewWithBottomPadding: React.ForwardRefExoticComponent<{
14
20
  /** Absolute Y content offset (iOS only, for KeyboardChatScrollView). */
15
21
  contentOffsetY?: SharedValue<number>;
16
22
  applyWorkaroundForContentInsetHitTestBug?: boolean;
23
+ /**
24
+ * Fires whenever the effective content inset changes (combines the static `contentInset`
25
+ * prop with the dynamic keyboard-driven padding). Useful on Android where the synthetic
26
+ * inset is not reflected in `onScroll` events.
27
+ */
28
+ onContentInsetChange?: (insets: ScrollViewContentInsets) => void;
17
29
  } & ScrollViewProps & React.RefAttributes<Reanimated.ScrollView>>;
18
30
  export default ScrollViewWithBottomPadding;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-keyboard-controller",
3
- "version": "1.21.5",
3
+ "version": "1.21.7",
4
4
  "description": "Keyboard manager which works in identical way on both iOS and Android",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
@@ -12,6 +12,7 @@ import useCombinedRef from "../hooks/useCombinedRef";
12
12
  import ScrollViewWithBottomPadding from "../ScrollViewWithBottomPadding";
13
13
 
14
14
  import { useChatKeyboard } from "./useChatKeyboard";
15
+ import { useEndVisible } from "./useEndVisible";
15
16
  import { useExtraContentPadding } from "./useExtraContentPadding";
16
17
 
17
18
  import type { KeyboardChatScrollViewProps } from "./types";
@@ -37,12 +38,16 @@ const KeyboardChatScrollView = forwardRef<
37
38
  applyWorkaroundForContentInsetHitTestBug = false,
38
39
  onLayout: onLayoutProp,
39
40
  onContentSizeChange: onContentSizeChangeProp,
41
+ onEndVisible,
40
42
  ...rest
41
43
  },
42
44
  ref,
43
45
  ) => {
44
46
  const scrollViewRef = useAnimatedRef<Reanimated.ScrollView>();
45
47
  const onRef = useCombinedRef(ref, scrollViewRef);
48
+ const freezeSV = useDerivedValue(() =>
49
+ typeof freeze === "boolean" ? freeze : freeze.value,
50
+ );
46
51
  const {
47
52
  padding,
48
53
  currentHeight,
@@ -55,7 +60,7 @@ const KeyboardChatScrollView = forwardRef<
55
60
  } = useChatKeyboard(scrollViewRef, {
56
61
  inverted,
57
62
  keyboardLiftBehavior,
58
- freeze,
63
+ freeze: freezeSV,
59
64
  offset,
60
65
  blankSpace,
61
66
  extraContentPadding,
@@ -72,7 +77,15 @@ const KeyboardChatScrollView = forwardRef<
72
77
  contentOffsetY,
73
78
  inverted,
74
79
  keyboardLiftBehavior,
75
- freeze,
80
+ freeze: freezeSV,
81
+ });
82
+
83
+ useEndVisible({
84
+ scroll,
85
+ layout,
86
+ size,
87
+ inverted,
88
+ onEndVisible,
76
89
  });
77
90
 
78
91
  const totalPadding = useDerivedValue(() =>
@@ -1,4 +1,7 @@
1
- import type { AnimatedScrollViewComponent } from "../ScrollViewWithBottomPadding";
1
+ import type {
2
+ AnimatedScrollViewComponent,
3
+ ScrollViewContentInsets,
4
+ } from "../ScrollViewWithBottomPadding";
2
5
  import type { KeyboardLiftBehavior } from "./useChatKeyboard/types";
3
6
  import type { ScrollViewProps } from "react-native";
4
7
  import type { SharedValue } from "react-native-reanimated";
@@ -46,7 +49,7 @@ export type KeyboardChatScrollViewProps = {
46
49
  *
47
50
  * Default is `false`.
48
51
  */
49
- freeze?: boolean;
52
+ freeze?: boolean | SharedValue<boolean>;
50
53
  /**
51
54
  * A shared value representing additional padding from external elements
52
55
  * (e.g., a growing multiline `TextInput` in a `KeyboardStickyView`).
@@ -87,4 +90,28 @@ export type KeyboardChatScrollViewProps = {
87
90
  * Default is `undefined` (equivalent to `0` — no minimum floor).
88
91
  */
89
92
  blankSpace?: SharedValue<number>;
93
+ /**
94
+ * Fires whenever the effective content inset changes — the static `contentInset`
95
+ * prop combined with the dynamic keyboard-driven padding.
96
+ *
97
+ * Useful on Android, where the synthetic content inset is not reflected in the native
98
+ * `onScroll` event payload. Consumers such as virtualized lists computing their own
99
+ * `scrollToEnd` target can use this to track the current inset alongside scroll offsets.
100
+ */
101
+ onContentInsetChange?: (insets: ScrollViewContentInsets) => void;
102
+ /**
103
+ * Fires whenever the visibility of the content "end" changes — both when the user
104
+ * arrives at the end and when they leave it. The boolean parameter reflects the new state.
105
+ *
106
+ * For non-inverted lists, the "end" is the bottom of the content. For inverted lists
107
+ * (`inverted={true}`), the "end" is the top of the scroll view, where the latest messages
108
+ * are rendered. The same internal detection drives `keyboardLiftBehavior="whenAtEnd"`.
109
+ *
110
+ * The callback can be either a plain JS function or a Reanimated worklet — the type is
111
+ * detected automatically. Worklets run on the UI thread; plain functions are dispatched
112
+ * via `runOnJS`.
113
+ *
114
+ * Fires once on mount with the initial state (after the scroll view has been measured).
115
+ */
116
+ onEndVisible?: (visible: boolean) => void;
90
117
  } & ScrollViewProps;
@@ -64,7 +64,7 @@ function useChatKeyboard(
64
64
  onStart: (e) => {
65
65
  "worklet";
66
66
 
67
- if (freeze) {
67
+ if (freeze.value) {
68
68
  return;
69
69
  }
70
70
 
@@ -229,7 +229,7 @@ function useChatKeyboard(
229
229
  onEnd: (e) => {
230
230
  "worklet";
231
231
 
232
- if (freeze) {
232
+ if (freeze.value) {
233
233
  return;
234
234
  }
235
235
 
@@ -242,7 +242,7 @@ function useChatKeyboard(
242
242
  padding.value = effective;
243
243
  },
244
244
  },
245
- [inverted, keyboardLiftBehavior, freeze, offset, extraContentPadding],
245
+ [inverted, keyboardLiftBehavior, offset, extraContentPadding],
246
246
  );
247
247
 
248
248
  return {
@@ -82,7 +82,7 @@ function useChatKeyboard(
82
82
  onStart: (e) => {
83
83
  "worklet";
84
84
 
85
- if (freeze) {
85
+ if (freeze.value) {
86
86
  return;
87
87
  }
88
88
 
@@ -166,7 +166,7 @@ function useChatKeyboard(
166
166
  onMove: (e) => {
167
167
  "worklet";
168
168
 
169
- if (freeze) {
169
+ if (freeze.value) {
170
170
  return;
171
171
  }
172
172
 
@@ -340,7 +340,7 @@ function useChatKeyboard(
340
340
  onEnd: (e) => {
341
341
  "worklet";
342
342
 
343
- if (freeze) {
343
+ if (freeze.value) {
344
344
  return;
345
345
  }
346
346
 
@@ -358,7 +358,7 @@ function useChatKeyboard(
358
358
  }
359
359
  },
360
360
  },
361
- [inverted, keyboardLiftBehavior, freeze, offset],
361
+ [inverted, keyboardLiftBehavior, offset],
362
362
  );
363
363
 
364
364
  return {
@@ -6,7 +6,7 @@ type KeyboardLiftBehavior = "always" | "whenAtEnd" | "persistent" | "never";
6
6
  type UseChatKeyboardOptions = {
7
7
  inverted: boolean;
8
8
  keyboardLiftBehavior: KeyboardLiftBehavior;
9
- freeze: boolean;
9
+ freeze: SharedValue<boolean>;
10
10
  offset: number;
11
11
  blankSpace: SharedValue<number>;
12
12
  /** Extra content padding shared value — needed on iOS to correctly clamp contentOffset. */
@@ -0,0 +1,66 @@
1
+ import { useMemo } from "react";
2
+ import {
3
+ runOnJS,
4
+ useAnimatedReaction,
5
+ useDerivedValue,
6
+ } from "react-native-reanimated";
7
+
8
+ import { isScrollAtEnd } from "./useChatKeyboard/helpers";
9
+
10
+ import type { SharedValue } from "react-native-reanimated";
11
+
12
+ type EndVisibleCallback = (visible: boolean) => void;
13
+
14
+ type Options = {
15
+ scroll: SharedValue<number>;
16
+ layout: SharedValue<{ width: number; height: number }>;
17
+ size: SharedValue<{ width: number; height: number }>;
18
+ inverted: boolean;
19
+ onEndVisible?: EndVisibleCallback;
20
+ };
21
+
22
+ const hasWorkletHash = (value: unknown): boolean =>
23
+ typeof value === "function" &&
24
+ !!(value as unknown as Record<string, unknown>).__workletHash;
25
+
26
+ export const useEndVisible = ({
27
+ scroll,
28
+ layout,
29
+ size,
30
+ inverted,
31
+ onEndVisible,
32
+ }: Options) => {
33
+ const isWorklet = useMemo(() => hasWorkletHash(onEndVisible), [onEndVisible]);
34
+
35
+ const isAtEnd = useDerivedValue(() => {
36
+ // Wait until the scroll view has been measured to avoid a spurious initial
37
+ // `true` on a (0,0,0) layout (the helper would otherwise treat unmeasured
38
+ // state as "at end" because 0 + 0 >= 0 - threshold).
39
+ if (layout.value.height === 0 || size.value.height === 0) {
40
+ return null;
41
+ }
42
+
43
+ return isScrollAtEnd(
44
+ scroll.value,
45
+ layout.value.height,
46
+ size.value.height,
47
+ inverted,
48
+ );
49
+ });
50
+
51
+ useAnimatedReaction(
52
+ () => isAtEnd.value,
53
+ (current, previous) => {
54
+ if (current === null || current === previous || !onEndVisible) {
55
+ return;
56
+ }
57
+
58
+ if (isWorklet) {
59
+ onEndVisible(current);
60
+ } else {
61
+ runOnJS(onEndVisible)(current);
62
+ }
63
+ },
64
+ [onEndVisible, isWorklet, inverted],
65
+ );
66
+ };
@@ -26,7 +26,7 @@ type UseExtraContentPaddingOptions = {
26
26
  contentOffsetY?: SharedValue<number>;
27
27
  inverted: boolean;
28
28
  keyboardLiftBehavior: KeyboardLiftBehavior;
29
- freeze: boolean;
29
+ freeze: SharedValue<boolean>;
30
30
  };
31
31
 
32
32
  /**
@@ -81,7 +81,7 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void {
81
81
  useAnimatedReaction(
82
82
  () => extraContentPadding.value,
83
83
  (current, previous) => {
84
- if (freeze || previous === null) {
84
+ if (freeze.value || previous === null) {
85
85
  return;
86
86
  }
87
87
 
@@ -141,7 +141,7 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void {
141
141
  scrollToTarget(target);
142
142
  }
143
143
  },
144
- [inverted, keyboardLiftBehavior, freeze],
144
+ [inverted, keyboardLiftBehavior],
145
145
  );
146
146
  }
147
147
 
@@ -1,7 +1,10 @@
1
1
  import React, { forwardRef, useMemo } from "react";
2
- import { Animated } from "react-native";
2
+ import Reanimated, {
3
+ interpolate,
4
+ useAnimatedStyle,
5
+ } from "react-native-reanimated";
3
6
 
4
- import { useKeyboardAnimation } from "../../hooks";
7
+ import { useReanimatedKeyboardAnimation } from "../../hooks";
5
8
 
6
9
  import type { View, ViewProps } from "react-native";
7
10
 
@@ -51,29 +54,25 @@ const KeyboardStickyView = forwardRef<
51
54
  },
52
55
  ref,
53
56
  ) => {
54
- const { height, progress } = useKeyboardAnimation();
57
+ const { height, progress } = useReanimatedKeyboardAnimation();
55
58
 
56
- const offset = progress.interpolate({
57
- inputRange: [0, 1],
58
- outputRange: [closed, opened],
59
- });
59
+ const stickyViewStyle = useAnimatedStyle(() => {
60
+ const offset = interpolate(progress.value, [0, 1], [closed, opened]);
60
61
 
61
- const styles = useMemo(() => {
62
- const disabled = Animated.add(Animated.multiply(height, 0), closed);
63
- const active = Animated.add(height, offset);
62
+ return {
63
+ transform: [{ translateY: enabled ? height.value + offset : closed }],
64
+ };
65
+ }, [closed, opened, enabled]);
64
66
 
65
- return [
66
- {
67
- transform: [{ translateY: enabled ? active : disabled }],
68
- },
69
- style,
70
- ];
71
- }, [closed, enabled, height, offset, style]);
67
+ const styles = useMemo(
68
+ () => [style, stickyViewStyle],
69
+ [style, stickyViewStyle],
70
+ );
72
71
 
73
72
  return (
74
- <Animated.View ref={ref} style={styles} {...props}>
73
+ <Reanimated.View ref={ref} style={styles} {...props}>
75
74
  {children}
76
- </Animated.View>
75
+ </Reanimated.View>
77
76
  );
78
77
  },
79
78
  );
@@ -99,11 +99,13 @@ const KeyboardToolbar: React.FC<KeyboardToolbarProps> & {
99
99
  );
100
100
  const containerStyle = useMemo(
101
101
  () => [
102
+ // CRUCIAL: gives the native view real bounds
103
+ styles.sticky,
102
104
  KEYBOARD_HAS_ROUNDED_CORNERS
103
- ? {
104
- marginLeft: (insets?.left ?? 0) + 16,
105
- marginRight: (insets?.right ?? 0) + 16,
106
- }
105
+ ? ({
106
+ left: (insets?.left ?? 0) + 16,
107
+ right: (insets?.right ?? 0) + 16,
108
+ } as const)
107
109
  : null,
108
110
  ],
109
111
  [insets],
@@ -207,6 +209,13 @@ const KeyboardToolbar: React.FC<KeyboardToolbarProps> & {
207
209
  };
208
210
 
209
211
  const styles = StyleSheet.create({
212
+ sticky: {
213
+ position: "absolute",
214
+ left: 0,
215
+ right: 0,
216
+ bottom: 0,
217
+ height: KEYBOARD_TOOLBAR_HEIGHT,
218
+ },
210
219
  toolbar: {
211
220
  position: "absolute",
212
221
  bottom: 0,
@@ -1,7 +1,10 @@
1
1
  import React, { forwardRef } from "react";
2
- import { Platform, View } from "react-native";
2
+ import { Platform } from "react-native";
3
3
  import Reanimated, {
4
+ runOnJS,
4
5
  useAnimatedProps,
6
+ useAnimatedReaction,
7
+ useDerivedValue,
5
8
  useSharedValue,
6
9
  } from "react-native-reanimated";
7
10
 
@@ -26,6 +29,13 @@ export type AnimatedScrollViewComponent = React.ForwardRefExoticComponent<
26
29
  AnimatedScrollViewProps & React.RefAttributes<Reanimated.ScrollView>
27
30
  >;
28
31
 
32
+ export type ScrollViewContentInsets = {
33
+ top: number;
34
+ bottom: number;
35
+ left: number;
36
+ right: number;
37
+ };
38
+
29
39
  type ScrollViewWithBottomPaddingProps = {
30
40
  ScrollViewComponent: AnimatedScrollViewComponent;
31
41
  children?: React.ReactNode;
@@ -36,6 +46,12 @@ type ScrollViewWithBottomPaddingProps = {
36
46
  /** Absolute Y content offset (iOS only, for KeyboardChatScrollView). */
37
47
  contentOffsetY?: SharedValue<number>;
38
48
  applyWorkaroundForContentInsetHitTestBug?: boolean;
49
+ /**
50
+ * Fires whenever the effective content inset changes (combines the static `contentInset`
51
+ * prop with the dynamic keyboard-driven padding). Useful on Android where the synthetic
52
+ * inset is not reflected in `onScroll` events.
53
+ */
54
+ onContentInsetChange?: (insets: ScrollViewContentInsets) => void;
39
55
  } & ScrollViewProps;
40
56
 
41
57
  const ScrollViewWithBottomPadding = forwardRef<
@@ -52,6 +68,7 @@ const ScrollViewWithBottomPadding = forwardRef<
52
68
  inverted,
53
69
  contentOffsetY,
54
70
  applyWorkaroundForContentInsetHitTestBug,
71
+ onContentInsetChange,
55
72
  children,
56
73
  ...rest
57
74
  },
@@ -59,11 +76,52 @@ const ScrollViewWithBottomPadding = forwardRef<
59
76
  ) => {
60
77
  const prevContentOffsetY = useSharedValue<number | null>(null);
61
78
 
79
+ const insets = useDerivedValue(() => {
80
+ const dynamicTop = inverted ? bottomPadding.value : 0;
81
+ const dynamicBottom = !inverted ? bottomPadding.value : 0;
82
+
83
+ return {
84
+ dynamic: {
85
+ top: dynamicTop,
86
+ bottom: dynamicBottom,
87
+ },
88
+ effective: {
89
+ top: dynamicTop + (contentInset?.top || 0),
90
+ bottom: dynamicBottom + (contentInset?.bottom || 0),
91
+ left: contentInset?.left || 0,
92
+ right: contentInset?.right || 0,
93
+ } as ScrollViewContentInsets,
94
+ };
95
+ }, [
96
+ inverted,
97
+ contentInset?.top,
98
+ contentInset?.bottom,
99
+ contentInset?.left,
100
+ contentInset?.right,
101
+ ]);
102
+
103
+ useAnimatedReaction(
104
+ () => insets.value.effective,
105
+ (current, previous) => {
106
+ if (!onContentInsetChange) {
107
+ return;
108
+ }
109
+ if (
110
+ previous &&
111
+ current.top === previous.top &&
112
+ current.bottom === previous.bottom &&
113
+ current.left === previous.left &&
114
+ current.right === previous.right
115
+ ) {
116
+ return;
117
+ }
118
+ runOnJS(onContentInsetChange)(current);
119
+ },
120
+ [onContentInsetChange],
121
+ );
122
+
62
123
  const animatedProps = useAnimatedProps(() => {
63
- const insetTop = inverted ? bottomPadding.value : 0;
64
- const insetBottom = !inverted ? bottomPadding.value : 0;
65
- const bottom = insetBottom + (contentInset?.bottom || 0);
66
- const top = insetTop + (contentInset?.top || 0);
124
+ const { dynamic, effective } = insets.value;
67
125
 
68
126
  const indicatorPadding = scrollIndicatorPadding ?? bottomPadding;
69
127
  const indicatorTop =
@@ -75,12 +133,7 @@ const ScrollViewWithBottomPadding = forwardRef<
75
133
 
76
134
  const result: Record<string, unknown> = {
77
135
  // iOS prop
78
- contentInset: {
79
- bottom: bottom,
80
- top: top,
81
- right: contentInset?.right,
82
- left: contentInset?.left,
83
- },
136
+ contentInset: effective,
84
137
  scrollIndicatorInsets: {
85
138
  bottom: indicatorBottom,
86
139
  top: indicatorTop,
@@ -88,8 +141,8 @@ const ScrollViewWithBottomPadding = forwardRef<
88
141
  left: scrollIndicatorInsets?.left,
89
142
  },
90
143
  // Android prop
91
- contentInsetBottom: insetBottom,
92
- contentInsetTop: insetTop,
144
+ contentInsetBottom: dynamic.bottom,
145
+ contentInsetTop: dynamic.top,
93
146
  };
94
147
 
95
148
  if (contentOffsetY) {
@@ -104,10 +157,6 @@ const ScrollViewWithBottomPadding = forwardRef<
104
157
 
105
158
  return result;
106
159
  }, [
107
- contentInset?.bottom,
108
- contentInset?.top,
109
- contentInset?.right,
110
- contentInset?.left,
111
160
  scrollIndicatorInsets?.bottom,
112
161
  scrollIndicatorInsets?.top,
113
162
  scrollIndicatorInsets?.right,
@@ -125,17 +174,7 @@ const ScrollViewWithBottomPadding = forwardRef<
125
174
  style={styles.container}
126
175
  >
127
176
  <ScrollViewComponent ref={ref} animatedProps={animatedProps} {...rest}>
128
- {inverted ? (
129
- // The only thing it can break is `StickyHeader`, but it's already broken in FlatList and other lists
130
- // don't support this functionality, so we can add additional view here
131
- // The correct fix would be to add a new prop in ScrollView that allows
132
- // to customize children extraction logic and skip custom view
133
- <View collapsable={false} nativeID="container">
134
- {children}
135
- </View>
136
- ) : (
137
- children
138
- )}
177
+ {children}
139
178
  </ScrollViewComponent>
140
179
  </ReanimatedClippingScrollView>
141
180
  );