Skip to content
Draft
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
4 changes: 4 additions & 0 deletions jest/setupMockLegendList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ export default function setupMockLegendList() {
};
});

jest.mock('@legendapp/list/keyboard', () => ({
KeyboardAwareLegendList: jest.requireMock<typeof LegendListModule>('@legendapp/list/react-native').LegendList,
}));

jest.mock('@legendapp/list/reanimated', () => ({
AnimatedLegendList: jest.requireMock<typeof LegendListModule>('@legendapp/list/react-native').LegendList,
}));
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@
"react-native-image-picker": "^7.1.2",
"react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#8a1c3b7359977462cdd4e63b09ea45afbd3d77c7",
"react-native-key-command": "1.0.14",
"react-native-keyboard-controller": "1.22.4",
"react-native-keyboard-controller": "1.23.0",
"react-native-localize": "^3.5.4",
"react-native-nitro-fetch": "1.6.2",
"react-native-nitro-image": "0.15.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
diff --git a/node_modules/@legendapp/list/keyboard.js b/node_modules/@legendapp/list/keyboard.js
--- a/node_modules/@legendapp/list/keyboard.js
+++ b/node_modules/@legendapp/list/keyboard.js
@@ -132,6 +132,8 @@
freeze,
keyboardLiftBehavior,
keyboardOffset,
+ onContentInsetChange: onContentInsetChangeProp,
+ ScrollViewComponent,
refScrollView,
...rest
} = props;
@@ -236,8 +238,9 @@
} else {
list == null ? void 0 : list.reportContentInset(insets);
}
- },
- [endScrollFreeze, getEndInset, keyboard, keyboardTarget]
+ onContentInsetChangeProp == null ? void 0 : onContentInsetChangeProp(insets);
+ },
+ [endScrollFreeze, getEndInset, keyboard, keyboardTarget, onContentInsetChangeProp]
);
const reportClosingInset = React.useCallback((bottom) => {
var _a2;
@@ -295,6 +298,7 @@
freeze,
keyboardLiftBehavior,
offset: keyboardOffset,
+ ScrollViewComponent,
onContentInsetChange,
onContentSizeChange: (width, height) => {
var _a2;
@@ -317,6 +321,7 @@
freeze,
keyboardLiftBehavior,
keyboardOffset,
+ ScrollViewComponent,
onContentInsetChange
]
);
diff --git a/node_modules/@legendapp/list/keyboard.mjs b/node_modules/@legendapp/list/keyboard.mjs
--- a/node_modules/@legendapp/list/keyboard.mjs
+++ b/node_modules/@legendapp/list/keyboard.mjs
@@ -111,6 +111,8 @@
freeze,
keyboardLiftBehavior,
keyboardOffset,
+ onContentInsetChange: onContentInsetChangeProp,
+ ScrollViewComponent,
refScrollView,
...rest
} = props;
@@ -215,8 +217,9 @@
} else {
list == null ? void 0 : list.reportContentInset(insets);
}
- },
- [endScrollFreeze, getEndInset, keyboard, keyboardTarget]
+ onContentInsetChangeProp == null ? void 0 : onContentInsetChangeProp(insets);
+ },
+ [endScrollFreeze, getEndInset, keyboard, keyboardTarget, onContentInsetChangeProp]
);
const reportClosingInset = useCallback((bottom) => {
var _a2;
@@ -274,6 +277,7 @@
freeze,
keyboardLiftBehavior,
offset: keyboardOffset,
+ ScrollViewComponent,
onContentInsetChange,
onContentSizeChange: (width, height) => {
var _a2;
@@ -296,6 +300,7 @@
freeze,
keyboardLiftBehavior,
keyboardOffset,
+ ScrollViewComponent,
onContentInsetChange
]
);
diff --git a/node_modules/@legendapp/list/keyboard.d.ts b/node_modules/@legendapp/list/keyboard.d.ts
--- a/node_modules/@legendapp/list/keyboard.d.ts
+++ b/node_modules/@legendapp/list/keyboard.d.ts
@@ -318,7 +318,11 @@
reportContentInset(inset?: Partial<Insets$1> | null): void;
};

-type KeyboardChatScrollViewPropsUnique = Omit<KeyboardChatScrollViewProps, keyof ScrollViewProps | "inverted" | "ScrollViewComponent" | "blankSpace" | "extraContentPadding" | "onContentInsetChange" | "offset">;
+type KeyboardChatScrollViewPropsUnique = Omit<KeyboardChatScrollViewProps, keyof ScrollViewProps | "inverted" | "ScrollViewComponent" | "blankSpace" | "extraContentPadding" | "offset">;
+type KeyboardAwareScrollViewComponentProps = NonNullable<KeyboardChatScrollViewProps["ScrollViewComponent"]> extends React.ForwardRefExoticComponent<infer Props> ? Omit<Props, "ref"> & {
+ ref?: React.Ref<React.ElementRef<typeof ScrollViewComponent>>;
+} : never;
+type KeyboardAwareScrollViewComponent = (props: KeyboardAwareScrollViewComponentProps) => React.ReactElement | null;
type ScrollMessageToEndOptions = {
animated: boolean;
closeKeyboard: boolean;
@@ -350,6 +354,7 @@
anchoredEndSpace?: AnchoredEndSpaceConfig;
contentInsetEndAdjustment?: SharedValue<number>;
keyboardOffset?: number;
+ ScrollViewComponent?: KeyboardAwareScrollViewComponent;
} & React.RefAttributes<LegendListRef>) => React.ReactElement | null;

export { KeyboardAwareLegendList, useKeyboardChatComposerInset, useKeyboardScrollToEnd };
11 changes: 10 additions & 1 deletion patches/@legendapp/list/details.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# `@legendapp/list` patches

Each patch is the build output of its upstream PR, so it changes exactly what that PR changes. Remove a patch once a LegendList release contains its PR.
Patches 001-007 match the build output of their upstream PRs. Patch 008 exposes the custom scroll view and inset callback needed by the keyboard integration.

### [@legendapp+list+3.7.2+001+apply-pending-anchor-correction.patch](@legendapp+list+3.7.2+001+apply-pending-anchor-correction.patch)

Expand Down Expand Up @@ -64,3 +64,12 @@ Each patch is the build output of its upstream PR, so it changes exactly what th
- E/App issue: 🛑
- PR introducing patch: https://github.com/Expensify/App/pull/100733
- Reproduction: On iOS or Android, open a chat with more history than the first page. Turn off the network connection. Turn the network connection back on. The messages on screen should not move when the placeholder above them disappears.

### [@legendapp+list+3.7.2+008+keyboard-scroll-view-component.patch](@legendapp+list+3.7.2+008+keyboard-scroll-view-component.patch)

- Reason: `KeyboardAwareLegendList` renders its own scroll view and doesn't accept a custom one. The patch exposes and forwards `ScrollViewComponent`, so RNKC drives the action-sheet-aware scroll view and message menus keep their keyboard spacing and scroll ref. It also forwards `onContentInsetChange` after updating the list's insets, so the Latest messages pill and unread tracking can refresh when the keyboard settles without another scroll event.
- Scope: Only `KeyboardAwareLegendList`, and only when `ScrollViewComponent` or `onContentInsetChange` is passed. LegendList's own inset reporting is unchanged on both platforms.
- Upstream PR/issue: 🛑
- E/App issue: https://github.com/Expensify/App/issues/54270
- PR introducing patch: 🛑
- Reproduction: On iOS, open a chat, focus the composer, and long-press a message. The message menu should open above the keyboard, and dragging the chat down should dismiss the keyboard with the composer attached.
23 changes: 19 additions & 4 deletions patches/react-native-keyboard-controller/details.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
# `react-native-keyboard-controller` patches

### [react-native-keyboard-controller+1.22.4+001+fix-insets-in-landscape.patch](react-native-keyboard-controller+1.22.4+001+fix-insets-in-landscape.patch)
### [react-native-keyboard-controller+1.23.0+001+fix-insets-in-landscape.patch](react-native-keyboard-controller+1.23.0+001+fix-insets-in-landscape.patch)

- Reason:

```
In EdgeToEdgeReactViewGroup.setupWindowInsets(), the library unconditionally applies navBarInsets.left and navBarInsets.right as margins on the content view, even though the bottom margin is already conditional on navigationBarTranslucent. In landscape mode on Android, the navigation bar moves to the side of the screen, and these unconditional margins shrink the content area by the nav bar width. This causes useSafeAreaFrame() to report a reduced windowWidth and useSafeAreaInsets() to return 0 for left/right insets, since the SafeAreaProvider view no longer overlaps with the nav bar area. The patch makes left/right margins respect the same shouldApplyZeroPaddingBottom condition used for the bottom margin.
Android: In EdgeToEdgeReactViewGroup.setupWindowInsets(), the library unconditionally applies navBarInsets.left and navBarInsets.right as margins on the content view, even though the bottom margin is already conditional on navigationBarTranslucent. In landscape mode on Android, the navigation bar moves to the side of the screen, and these unconditional margins shrink the content area by the nav bar width. This causes useSafeAreaFrame() to report a reduced windowWidth and useSafeAreaInsets() to return 0 for left/right insets, since the SafeAreaProvider view no longer overlaps with the nav bar area. The patch makes left/right margins respect the same shouldApplyZeroPaddingBottom condition used for the bottom margin.

```

- Upstream PR/issue: -
- Upstream PR/issue: 🛑
- E/App issue: https://github.com/Expensify/App/issues/87307
- PR introducing patch: https://github.com/Expensify/App/pull/87376

### [react-native-keyboard-controller+1.22.4+002+apply-initial-keyboard-state-on-ui-thread.patch](react-native-keyboard-controller+1.22.4+002+apply-initial-keyboard-state-on-ui-thread.patch)
### [react-native-keyboard-controller+1.23.0+002+apply-initial-keyboard-state-on-ui-thread.patch](react-native-keyboard-controller+1.23.0+002+apply-initial-keyboard-state-on-ui-thread.patch)

- Reason:

Expand All @@ -23,3 +24,17 @@
- Upstream PR/issue: https://github.com/kirillzyusko/react-native-keyboard-controller/pull/1685
- E/App issue: https://github.com/Expensify/App/pull/100733#issuecomment-6411009930
- PR introducing patch: https://github.com/Expensify/App/pull/106003

### [react-native-keyboard-controller+1.23.0+003+stabilize-ios-interactive-keyboard-state.patch](react-native-keyboard-controller+1.23.0+003+stabilize-ios-interactive-keyboard-state.patch)

- Reason: Rejects non-finite and negative keyboard layout-guide positions while preserving the upstream fractional-height rounding. Applies final keyboard events to Animated and Reanimated values so a cancelled dismissal returns the composer to its final position.
- Upstream PR/issue: https://github.com/kirillzyusko/react-native-keyboard-controller/issues/1563
- E/App issue: https://github.com/Expensify/App/issues/54270
- PR introducing patch: https://github.com/margelo/Expensify/pull/38

### [react-native-keyboard-controller+1.23.0+004+namespace-ios-ui-utils.patch](react-native-keyboard-controller+1.23.0+004+namespace-ios-ui-utils.patch)

- Reason: Prefixes the Objective-C helper name so the keyboard controller can link alongside HybridApp's existing UIUtils class. Swift callers keep the same API.
- Upstream PR/issue: https://github.com/kirillzyusko/react-native-keyboard-controller/pull/1686
- E/App issue: https://github.com/Expensify/App/issues/54270
- PR introducing patch: https://github.com/margelo/Expensify/pull/38
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
diff --git a/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt
index a8b8a7a..e227a3e 100644
--- a/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt
+++ b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt
@@ -119,13 +119,21 @@ class EdgeToEdgeReactViewGroup(
@@ -119,13 +119,21 @@
if (!shouldApplyBottomPadding) 0 else insets.getInsets(WindowInsetsCompat.Type.ime()).bottom

params.setMargins(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
diff --git a/node_modules/react-native-keyboard-controller/ios/observers/movement/KeyboardTrackingView.swift b/node_modules/react-native-keyboard-controller/ios/observers/movement/KeyboardTrackingView.swift
--- a/node_modules/react-native-keyboard-controller/ios/observers/movement/KeyboardTrackingView.swift
+++ b/node_modules/react-native-keyboard-controller/ios/observers/movement/KeyboardTrackingView.swift
@@ -141,7 +141,7 @@
// for `keyboardLayoutGuide` case we can just read keyboard position directly - no interpolation needed
if KeyboardControllerConfiguration.usesKeyboardLayoutGuideTracking {
// KVO and keyboard notifications can round fractional accessory heights differently.
- if round(keyboardPosition) > round(keyboardHeight) {
+ if !keyboardPosition.isFinite || keyboardPosition < 0 || round(keyboardPosition) > round(keyboardHeight) {
return Self.invalidPosition
}
// when we are the top position KVO takes `inputAccessoryView` into consideration,
diff --git a/node_modules/react-native-keyboard-controller/src/animated.tsx b/node_modules/react-native-keyboard-controller/src/animated.tsx
--- a/node_modules/react-native-keyboard-controller/src/animated.tsx
+++ b/node_modules/react-native-keyboard-controller/src/animated.tsx
@@ -178,7 +178,7 @@
onKeyboardMoveEnd: (event: NativeEvent) => {
"worklet";

- updateSharedValues(event, ["android"]);
+ updateSharedValues(event, ["android", "ios"]);
},
},
[],
@@ -226,7 +226,7 @@
onKeyboardMoveStart={OS === "ios" ? onKeyboardMove : undefined}
onKeyboardMove={OS === "android" ? onKeyboardMove : undefined}
onKeyboardMoveInteractive={onKeyboardMove}
- onKeyboardMoveEnd={OS === "android" ? onKeyboardMove : undefined}
+ onKeyboardMoveEnd={onKeyboardMove}
onFocusedInputLayoutChangedReanimated={inputLayoutHandler}
>
{children}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
diff --git a/node_modules/react-native-keyboard-controller/ios/core/UIUtils.swift b/node_modules/react-native-keyboard-controller/ios/core/UIUtils.swift
--- a/node_modules/react-native-keyboard-controller/ios/core/UIUtils.swift
+++ b/node_modules/react-native-keyboard-controller/ios/core/UIUtils.swift
@@ -8,7 +8,7 @@

import Foundation

-@objc(UIUtils)
+@objc(RNKCUIUtils)
public final class UIUtils: NSObject {
@objc public static let nextFrame = 1.0 / 60
}
diff --git a/node_modules/react-native-keyboard-controller/ios/KeyboardControllerModule.mm b/node_modules/react-native-keyboard-controller/ios/KeyboardControllerModule.mm
--- a/node_modules/react-native-keyboard-controller/ios/KeyboardControllerModule.mm
+++ b/node_modules/react-native-keyboard-controller/ios/KeyboardControllerModule.mm
@@ -156,7 +156,7 @@
#ifdef RCT_NEW_ARCH_ENABLED
// Layout events can precede Fabric mounting. Allow a display interval before the only lookup.
dispatch_after(
- dispatch_time(DISPATCH_TIME_NOW, (int64_t)(UIUtils.nextFrame * NSEC_PER_SEC)),
+ dispatch_time(DISPATCH_TIME_NOW, (int64_t)(RNKCUIUtils.nextFrame * NSEC_PER_SEC)),
dispatch_get_main_queue(),
measure);
#else
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import Reanimated from 'react-native-reanimated';

import {Actions, ActionSheetAwareScrollViewProvider, useActionSheetAwareScrollViewActions, useActionSheetAwareScrollViewState} from './ActionSheetAwareScrollViewContext';

const renderScrollComponent = undefined;

export {renderScrollComponent, ActionSheetAwareScrollViewProvider, Actions, useActionSheetAwareScrollViewState, useActionSheetAwareScrollViewActions};

export default Reanimated.ScrollView;
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import Reanimated from 'react-native-reanimated';

import {Actions, ActionSheetAwareScrollViewProvider, useActionSheetAwareScrollViewActions, useActionSheetAwareScrollViewState} from './ActionSheetAwareScrollViewContext';

const renderScrollComponent = undefined;

export {renderScrollComponent, ActionSheetAwareScrollViewProvider, Actions, useActionSheetAwareScrollViewState, useActionSheetAwareScrollViewActions};

export default Reanimated.ScrollView;
4 changes: 4 additions & 0 deletions src/components/ActionSheetAwareScrollView/__mocks__/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import Reanimated from 'react-native-reanimated';

import {Actions, ActionSheetAwareScrollViewProvider, useActionSheetAwareScrollViewActions, useActionSheetAwareScrollViewState} from './ActionSheetAwareScrollViewContext';

const renderScrollComponent = undefined;

export {renderScrollComponent, ActionSheetAwareScrollViewProvider, Actions, useActionSheetAwareScrollViewState, useActionSheetAwareScrollViewActions};

export default Reanimated.ScrollView;
2 changes: 2 additions & 0 deletions src/components/ActionSheetAwareScrollView/index.android.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ const renderScrollComponent: RenderActionSheetAwareScrollViewComponent = (props)
};

export {renderScrollComponent, ActionSheetAwareScrollViewProvider, Actions, useActionSheetAwareScrollViewState, useActionSheetAwareScrollViewActions};

export default ActionSheetAwareScrollView;
2 changes: 2 additions & 0 deletions src/components/ActionSheetAwareScrollView/index.ios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ const renderScrollComponent: RenderActionSheetAwareScrollViewComponent = (props)
};

export {renderScrollComponent, ActionSheetAwareScrollViewProvider, Actions, useActionSheetAwareScrollViewState, useActionSheetAwareScrollViewActions};

export default ActionSheetAwareScrollView;
2 changes: 2 additions & 0 deletions src/components/ActionSheetAwareScrollView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ const renderScrollComponent: RenderActionSheetAwareScrollViewComponent = (props)
};

export {renderScrollComponent, ActionSheetAwareScrollViewProvider, Actions, useActionSheetAwareScrollViewState, useActionSheetAwareScrollViewActions};

export default ActionSheetAwareScrollView;
8 changes: 4 additions & 4 deletions src/components/ActionSheetAwareScrollView/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import type {ActionWithPayload, State} from '@hooks/useWorkletStateMachine';

import type {ComponentRef, Ref} from 'react';
// eslint-disable-next-line no-restricted-imports
import type {ScrollView, ScrollViewProps} from 'react-native';
import type {ScrollView, ScrollViewComponent, ScrollViewProps} from 'react-native';
import type Reanimated from 'react-native-reanimated';
import type {SharedValue} from 'react-native-reanimated';
import type {AnimatedScrollViewProps, SharedValue} from 'react-native-reanimated';

type ActionSheetAwareScrollViewMeasurements = {
frameY?: number;
Expand All @@ -27,9 +27,9 @@ type ActionSheetAwareScrollViewActionsContextValue = {

type ActionSheetAwareScrollViewContextValue = ActionSheetAwareScrollViewStateContextValue & ActionSheetAwareScrollViewActionsContextValue;

type ActionSheetAwareScrollViewHandle = ComponentRef<typeof ScrollView> | Reanimated.ScrollView;
type ActionSheetAwareScrollViewHandle = ComponentRef<typeof ScrollView> | ComponentRef<typeof ScrollViewComponent> | Reanimated.ScrollView;

type ActionSheetAwareScrollViewProps = ScrollViewProps & {
type ActionSheetAwareScrollViewProps = Omit<AnimatedScrollViewProps, 'ref'> & {
ref?: Ref<ActionSheetAwareScrollViewHandle>;
};

Expand Down
Loading