Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ export const mockSize = { value: { width: 390, height: 2000 } };
export const KEYBOARD = 300;
export const mockScrollTo = jest.fn();

type Reaction = {
producer: () => unknown;
effect: (current: unknown, previous: unknown | null) => void;
previous: unknown;
};

const reactions: Reaction[] = [];

/**
* Linear interpolate mock matching Reanimated's `interpolate` signature.
*
Expand Down Expand Up @@ -61,17 +69,38 @@ export function reset() {
*/
export function setupBeforeEach() {
jest.resetModules();
reactions.length = 0;

jest.doMock("react-native-reanimated", () => ({
...require("react-native-reanimated/mock"),
scrollTo: mockScrollTo,
interpolate: mockInterpolate,
useAnimatedReaction: (
producer: () => unknown,
effect: (current: unknown, previous: unknown | null) => void,
) => {
reactions.push({
producer,
effect,
previous: producer(),
});
},
}));

reset();
mockScrollTo.mockClear();
}

/** Run registered Reanimated reactions after mutating mocked shared values. */
export function flushAnimatedReactions() {
for (const reaction of reactions) {
const current = reaction.producer();

reaction.effect(current, reaction.previous);
reaction.previous = current;
}
}

type RenderOptions = Omit<
Parameters<typeof useChatKeyboard>[1],
"freeze" | "offset" | "blankSpace" | "extraContentPadding"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { sv } from "../../../../__fixtures__/sv";
import {
type Handlers,
KEYBOARD,
createRender,
flushAnimatedReactions,
mockLayout,
mockOffset,
mockScrollTo,
Expand Down Expand Up @@ -77,4 +79,29 @@ describe("`useChatKeyboard` — Android freeze", () => {

expect(result.current.padding.value).toBe(0);
});

it("should apply the latest frozen keyboard padding when unfrozen", () => {
const freeze = sv(false);
const { result } = render({
inverted: false,
keyboardLiftBehavior: "always",
freeze,
});

handlers.onStart({ height: KEYBOARD });
handlers.onEnd({ height: KEYBOARD });
expect(result.current.padding.value).toBe(KEYBOARD);

freeze.value = true;
flushAnimatedReactions();
handlers.onStart({ height: 0 });
handlers.onMove({ height: 120 });
handlers.onEnd({ height: 0 });
expect(result.current.padding.value).toBe(KEYBOARD);

freeze.value = false;
flushAnimatedReactions();

expect(result.current.padding.value).toBe(0);
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { sv } from "../../../../__fixtures__/sv";
import {
type Handlers,
KEYBOARD,
createRender,
flushAnimatedReactions,
mockLayout,
mockOffset,
mockScrollTo,
Expand Down Expand Up @@ -103,6 +105,29 @@ describe("`useChatKeyboard` — iOS non-inverted + always", () => {

expect(mockScrollTo).not.toHaveBeenCalled();
});

it("should apply the latest frozen keyboard padding when unfrozen", () => {
const freeze = sv(false);
const { result } = render({
inverted: false,
keyboardLiftBehavior: "always",
freeze,
});

handlers.onStart({ height: KEYBOARD });
expect(result.current.padding.value).toBe(KEYBOARD);

freeze.value = true;
flushAnimatedReactions();
handlers.onStart({ height: 0 });
handlers.onEnd({ height: 0 });
expect(result.current.padding.value).toBe(KEYBOARD);

freeze.value = false;
flushAnimatedReactions();

expect(result.current.padding.value).toBe(0);
});
});

describe("`useChatKeyboard` — iOS inverted + always", () => {
Expand Down
34 changes: 26 additions & 8 deletions src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useSharedValue } from "react-native-reanimated";
import { useAnimatedReaction, useSharedValue } from "react-native-reanimated";

import { useKeyboardHandler } from "../../../hooks";
import useScrollState from "../../hooks/useScrollState";
Expand Down Expand Up @@ -64,15 +64,13 @@ function useChatKeyboard(
onStart: (e) => {
"worklet";

if (freeze.value) {
return;
}

if (e.height > 0) {
// eslint-disable-next-line react-compiler/react-compiler
targetKeyboardHeight.value = e.height;
}

currentHeight.value = e.height;

const effective = getEffectiveHeight(
e.height,
targetKeyboardHeight.value,
Expand Down Expand Up @@ -109,6 +107,10 @@ function useChatKeyboard(
effective + extraContentPadding.value,
);

if (freeze.value) {
return;
}

// persistent mode: when keyboard shrinks, clamp to valid range
if (
keyboardLiftBehavior === "persistent" &&
Expand Down Expand Up @@ -229,22 +231,38 @@ function useChatKeyboard(
onEnd: (e) => {
"worklet";

if (freeze.value) {
return;
}
currentHeight.value = e.height;

const effective = getEffectiveHeight(
e.height,
targetKeyboardHeight.value,
offset,
);

if (freeze.value) {
return;
}

padding.value = effective;
},
},
[inverted, keyboardLiftBehavior, offset, extraContentPadding],
);

useAnimatedReaction(
() => freeze.value,
(isFrozen, wasFrozen) => {
if (!isFrozen && wasFrozen === true) {
padding.value = getEffectiveHeight(
currentHeight.value,
targetKeyboardHeight.value,
offset,
);
}
},
[offset],
);
Comment on lines +252 to +264

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is identical at the moment between iOS/Android? Would it make sense to move it into separate useFrozenPadding hook? Similar how code organized in useExtraContentPadding/useEndVisible? What do you think about it?


return {
padding,
currentHeight,
Expand Down
49 changes: 37 additions & 12 deletions src/components/KeyboardChatScrollView/useChatKeyboard/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { scrollTo, useSharedValue } from "react-native-reanimated";
import {
scrollTo,
useAnimatedReaction,
useSharedValue,
} from "react-native-reanimated";

import { useKeyboardHandler } from "../../../hooks";
import useScrollState from "../../hooks/useScrollState";
Expand Down Expand Up @@ -82,13 +86,10 @@ function useChatKeyboard(
onStart: (e) => {
"worklet";

if (freeze.value) {
return;
}

if (e.height > 0) {
// eslint-disable-next-line react-compiler/react-compiler
targetKeyboardHeight.value = e.height;
currentHeight.value = e.height;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to update it inside onStart? We also update it inside onMove, so it would be strange to have following sequence of changes like 336, 0, 80, 120 ... 336?

closing.value = false;
} else {
closing.value = true;
Expand Down Expand Up @@ -128,6 +129,10 @@ function useChatKeyboard(
minimumPaddingAbsorbed,
);

if (freeze.value) {
return;
}

if (inverted && e.duration === -1) {
// Android inverted: skip post-interactive snap-back events
// (duration === -1 means the keyboard is re-establishing its
Expand Down Expand Up @@ -166,10 +171,6 @@ function useChatKeyboard(
onMove: (e) => {
"worklet";

if (freeze.value) {
return;
}

currentHeight.value = e.height;

if (inverted) {
Expand All @@ -184,6 +185,10 @@ function useChatKeyboard(
offset,
);

if (freeze.value) {
return;
}

const minimumPaddingAbsorbed =
getMinimumPaddingAbsorbed(
blankSpace.value,
Expand Down Expand Up @@ -269,6 +274,10 @@ function useChatKeyboard(
offset,
);

if (freeze.value) {
return;
}

const minimumPaddingAbsorbed =
getMinimumPaddingAbsorbed(
blankSpace.value,
Expand Down Expand Up @@ -340,16 +349,18 @@ function useChatKeyboard(
onEnd: (e) => {
"worklet";

if (freeze.value) {
return;
}
currentHeight.value = e.height;

const effective = getEffectiveHeight(
e.height,
targetKeyboardHeight.value,
offset,
);

if (freeze.value) {
return;
}

padding.value = effective;

// Record actual scroll displacement so close can be symmetric
Expand All @@ -361,6 +372,20 @@ function useChatKeyboard(
[inverted, keyboardLiftBehavior, offset],
);

useAnimatedReaction(
() => freeze.value,
(isFrozen, wasFrozen) => {
if (!isFrozen && wasFrozen === true) {
padding.value = getEffectiveHeight(
currentHeight.value,
targetKeyboardHeight.value,
offset,
);
}
},
[offset],
);

return {
padding,
currentHeight,
Expand Down
Loading