Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
7fc2c0c
feat: migrate lists to interactive keyboard dismissal
chrispader Sep 28, 2025
2617c04
fix: typo
chrispader Sep 28, 2025
60534f4
Merge branch 'main' into pr/71440
chrispader Oct 16, 2025
6ba7f7d
fix: invalid import
chrispader Oct 16, 2025
41febe0
Merge branch 'main' into pr/71440
chrispader Oct 17, 2025
0dfa732
Merge branch 'main' into pr/71440
chrispader Oct 28, 2025
4762b4c
Merge branch 'main' into pr/71440
chrispader Jan 28, 2026
85942a8
refactor: adapt report footer style types
chrispader Jan 29, 2026
23c95a4
refactor: types of `getReportPaddingBottom`
chrispader Jan 29, 2026
30f12da
refactor: extract variable for more readability
chrispader Jan 29, 2026
4e9dc64
fix: invalid import
chrispader Jan 29, 2026
d32d875
fix: remove unused import
chrispader Jan 29, 2026
6e3b74c
refactor: match condition on main
chrispader Jan 29, 2026
153886d
fix: use platform specific `shouldEnableKeyboardAvoidingViewResult`
chrispader Jan 29, 2026
4ea116f
refactor: evaluate keyboardHeight once in style util function
chrispader Jan 29, 2026
7cf181e
refactor: update relative import
chrispader Jan 29, 2026
63148a9
refactor: `nativeID` properties
chrispader Jan 29, 2026
a4e17d1
refactor: changes from main
chrispader Jan 29, 2026
36ce4da
fix: enable animated keyboard dismissal in ReportActionsList
chrispader Jan 29, 2026
efc4cb0
fix: RNKC invalid `onMove` and `onInteractive` events
chrispader Jan 29, 2026
95a6510
fix: don't completely block `onInteractive` events
chrispader Jan 29, 2026
59d5516
add RNKC issue link
chrispader Jan 29, 2026
51bdf1f
fix: missing `onLayout` prop in tests
chrispader Jan 29, 2026
16e950e
fix: spell check
chrispader Jan 29, 2026
dd6ec32
fix: remove deprecated `runOnJS`
chrispader Jan 29, 2026
3a281e2
fix: worklet callback
chrispader Jan 29, 2026
80e06eb
fix: pass missing `inverted` property on Android
chrispader Jan 29, 2026
1a36843
fix: remove unnecessary web only composer scroll events
chrispader Jan 29, 2026
71daa9d
chore: add `react-native-keyboard-controller` patch for invalid `onIn…
chrispader Jan 30, 2026
ed83e15
fix: remove `onInteractive` workaround
chrispader Jan 30, 2026
a0ffcd7
move patch and add `details.md`
chrispader Jan 30, 2026
eee157d
remove constant
chrispader Jan 30, 2026
6c96aad
Merge branch 'main' into pr/71440
chrispader Feb 3, 2026
1f2286e
chore: update patch version
chrispader Feb 3, 2026
0ea917a
fix: move eslint ignore comment
chrispader Feb 3, 2026
1b077c3
Create react-native-keyboard-controller+1.20.6+002+fix-invalid-onInte…
chrispader Feb 6, 2026
e20083c
Merge branch 'main' into pr/71440
chrispader Feb 11, 2026
37175dc
chore: remove no more needed patches
chrispader Feb 12, 2026
30ac58b
chore: remove RNKC `details.md`
chrispader Feb 12, 2026
b473dac
Merge branch 'main' into pr/71440
chrispader Feb 16, 2026
b858dbb
Merge branch 'main' into pr/71440
chrispader Mar 9, 2026
cab42cf
fix: TS and lint errors
chrispader Mar 9, 2026
b0e36a7
Merge branch 'main' into pr/71440
chrispader Jul 14, 2026
b487499
fix: missing type import
chrispader Jul 14, 2026
1f74447
chore: start KeyboardDismisableFlashList
chrispader Jul 14, 2026
c69ab53
fix: TS error in `AnimatedFlashListWithCellRenderer`
chrispader Jul 15, 2026
e2c061e
Merge branch 'main' into pr/71440
chrispader Jul 15, 2026
dc2d364
chore: update RNKC patch
chrispader Jul 15, 2026
68115c8
chore: update RNKC
chrispader Jul 15, 2026
3a6e64f
refactor: replace FlatList with FlashList context wrapper
chrispader Jul 15, 2026
89e15c7
chore: merge main into interactive keyboard dismissal
chrispader Sep 9, 2026
b5053ee
chore: merge latest main
chrispader Sep 9, 2026
ab21349
chore: merge latest main
chrispader Sep 9, 2026
b83f3b1
fix: make animated FlashList compiler compliant
chrispader Sep 9, 2026
b1275db
chore: merge latest main
chrispader Sep 9, 2026
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: 3 additions & 1 deletion config/eslint/eslint.seatbelt.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@
"../../src/components/AnchorForCommentsOnly/index.tsx" "no-restricted-syntax" 1
"../../src/components/AnimatedFlatListWithCellRenderer.tsx" "@typescript-eslint/no-deprecated/Animated.createAnimatedComponent" 1
"../../src/components/AnimatedFlatListWithCellRenderer.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/components/AnimatedFlashListWithCellRenderer.tsx" "@typescript-eslint/no-deprecated/Animated.createAnimatedComponent" 1
"../../src/components/AnimatedFlashListWithCellRenderer.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/AnimatedFlashListWithCellRenderer.tsx" "react-hooks/refs" 2
"../../src/components/AnimatedSubmitButton/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/AnimatedSubmitButton/index.tsx" "react-hooks/refs" 4
"../../src/components/AnimatedSubmitButton/index.tsx" "react-hooks/set-state-in-effect" 1
Expand Down Expand Up @@ -347,7 +350,6 @@
"../../src/components/SubStepForms/FullNameStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../src/components/SubStepForms/PushRowFieldsStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/SubStepForms/RegistrationNumberStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../src/components/SwipeableView/index.native.tsx" "react-hooks/refs" 2
"../../src/components/SymbolButton.tsx" "no-restricted-syntax" 1
"../../src/components/Table/middlewares/filtering.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/Table/Table.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4
Expand Down
2 changes: 2 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2998,6 +2998,8 @@ const CONST = {
BIG_SCREEN_SUGGESTION_WIDTH: 300,
},
COMPOSER_MAX_HEIGHT: 125,
// Starts with this value to avoid a big jump while the container height is being calculated in case the screen is first rendered w/ a full size composer. It's based on the perceived concierge header height on the iPhone 16 Pro.
CHAT_HEADER_BASE_HEIGHT: 73,
CHAT_FOOTER_SECONDARY_ROW_HEIGHT: 15,
CHAT_FOOTER_SECONDARY_ROW_PADDING: 5,
CHAT_FOOTER_MIN_HEIGHT: 65,
Expand Down
116 changes: 116 additions & 0 deletions src/components/AnimatedFlashListWithCellRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import type {FlashListRef, FlashListProps} from '@shopify/flash-list';
/**
* This is a copy of the FlashList implementation from 'react-native-reanimated' in order to implement a custom CellRendererComponent.
* This should be updated when the original implementation updates
* Taken from: https://github.com/software-mansion/react-native-reanimated/blob/main/packages/react-native-reanimated/src/component/FlashList.tsx
*/
import type {Ref} from 'react';
import type {CellRendererProps} from 'react-native';
import type {AnimatedProps, ILayoutAnimationBuilder} from 'react-native-reanimated';

import {FlashList} from '@shopify/flash-list';
import React, {createContext, useContext} from 'react';
import Animated, {LayoutAnimationConfig} from 'react-native-reanimated';

const AnimatedFlashList = Animated.createAnimatedComponent(FlashList);

type CellRendererComponentType<Item> = React.ComponentType<CellRendererProps<Item>> | null | undefined;

type CellRendererConfig = {
itemLayoutAnimation?: ILayoutAnimationBuilder;
outerCellRenderer?: CellRendererComponentType<unknown>;
};

const CellRendererConfigContext = createContext<CellRendererConfig>({});

function CellRendererComponentImpl(props: CellRendererProps<unknown> & {children: React.ReactNode}) {
const {itemLayoutAnimation, outerCellRenderer: OuterCellRenderer} = useContext(CellRendererConfigContext);

return (
<Animated.View
// @ts-expect-error TODO TYPESCRIPT This is temporary cast is to get rid of .d.ts file.
layout={itemLayoutAnimation as unknown}
onLayout={props.onLayout}
style={OuterCellRenderer ? undefined : props.style}
>
{OuterCellRenderer ? <OuterCellRenderer {...props}>{props.children}</OuterCellRenderer> : props.children}
</Animated.View>
);
}
type ReanimatedFlashListPropsWithLayout<T> = {
/**
* Lets you pass layout animation directly to the FlashList item.
*/
itemLayoutAnimation?: ILayoutAnimationBuilder;
/**
* Lets you skip entering and exiting animations of FlashList items when on FlashList mount or unmount.
*/
skipEnteringExitingAnimations?: boolean;
} & AnimatedProps<FlashListProps<T>>;

// Since createAnimatedComponent return type is ComponentClass that has the props of the argument,
// but not things like NativeMethods, etc. we need to add them manually by extending the type.

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnimatedFlashListWithCellRendererProps<Item = any> = Omit<ReanimatedFlashListPropsWithLayout<Item>, 'CellRendererComponent' | 'onScroll' | 'inverted'> & {
CellRendererComponent?: CellRendererComponentType<Item>;
onScroll?: FlashListProps<Item>['onScroll'];
inverted?: boolean | null | undefined;
ref?: Ref<FlashListRef<Item>>;
};

function FlashListRenderImpl(props: AnimatedFlashListWithCellRendererProps<unknown>) {
const {itemLayoutAnimation, skipEnteringExitingAnimations, ref, CellRendererComponent: outerCellRenderer, ...restProps} = props;

// Set default scrollEventThrottle, because user expects
// to have continuous scroll events and
// react-native defaults it to 50 for FlashLists.
// We set it to 1, so we have peace until
// there are 960 fps screens.
if (!('scrollEventThrottle' in restProps)) {
restProps.scrollEventThrottle = 1;
}

const cellRendererConfig: CellRendererConfig = {itemLayoutAnimation, outerCellRenderer};

const animatedFlashList = (
<CellRendererConfigContext.Provider value={cellRendererConfig}>
<AnimatedFlashList
{...restProps}
ref={ref}
CellRendererComponent={CellRendererComponentImpl}
/>
</CellRendererConfigContext.Provider>
);

if (skipEnteringExitingAnimations === undefined) {
return animatedFlashList;
}

return (
<LayoutAnimationConfig
skipEntering
skipExiting
>
{animatedFlashList}
</LayoutAnimationConfig>
);
}

// We need explicit any here, because this is the exact same type that is used in React Native types.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function FlashListRender<Item = any>(props: AnimatedFlashListWithCellRendererProps<Item>) {
return <FlashListRenderImpl {...(props as AnimatedFlashListWithCellRendererProps<unknown>)} />;
}

const AnimatedFlashListWithCellRenderer = FlashListRender as <
// We need explicit any here, because this is the exact same type that is used in React Native types.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ItemT = any,
>(
props: AnimatedFlashListWithCellRendererProps<ItemT>,
) => React.ReactElement;

export type {AnimatedFlashListWithCellRendererProps};

export default AnimatedFlashListWithCellRenderer;
3 changes: 2 additions & 1 deletion src/components/Composer/implementation/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ function Composer({
selection,
value,
isGroupPolicyReport = false,
nativeID,
ref,
...props
}: ComposerProps) {
Expand Down Expand Up @@ -132,7 +133,7 @@ function Composer({

return (
<RNMarkdownTextInput
id={CONST.COMPOSER.NATIVE_ID}
id={nativeID ?? CONST.COMPOSER.NATIVE_ID}
multiline
autoComplete="off"
placeholderTextColor={theme.placeholderText}
Expand Down
3 changes: 2 additions & 1 deletion src/components/Composer/implementation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ function Composer({
onContentSizeChange,
shouldContainScroll = true,
isGroupPolicyReport = false,
nativeID,
ref,
...props
}: ComposerProps) {
Expand Down Expand Up @@ -362,7 +363,7 @@ function Composer({

return (
<RNMarkdownTextInput
id={CONST.COMPOSER.NATIVE_ID}
nativeID={nativeID ?? CONST.COMPOSER.NATIVE_ID}
autoComplete="off"
autoCorrect={!isMobileSafari()}
placeholderTextColor={theme.placeholderText}
Expand Down
14 changes: 2 additions & 12 deletions src/components/FlashList/InvertedFlashList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
import type FlatListRefType from '@components/FlashList/types';

import type {FlashListProps} from '@shopify/flash-list';
import type {CustomFlashListProps} from '@components/FlashList/types';

import React from 'react';

import FlashList from '..';
import CellRendererComponent from './CellRendererComponent';

type InvertedFlashListProps<T> = FlashListProps<T> & {
data: T[];
keyExtractor: (item: T, index: number) => string;

/** Ref to the underlying list instance. */
ref: FlatListRefType;
};

function InvertedFlashList<T>(props: InvertedFlashListProps<T>) {
function InvertedFlashList<T>(props: CustomFlashListProps<T>) {
return (
<FlashList<T>
{...props}
Expand Down
18 changes: 16 additions & 2 deletions src/components/FlashList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import KeyboardDismissibleFlashList from '@components/KeyboardDismissibleFlashList';

import useEmitComposerScrollEvents from '@hooks/useEmitComposerScrollEvents';

import type {FlashListProps} from '@shopify/flash-list';
import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native';

import {FlashList as ShopifyFlashList} from '@shopify/flash-list';
import React from 'react';

function FlashList<T>({onScroll: onScrollProp, inverted, ...restProps}: FlashListProps<T>) {
import type {CustomFlashListProps} from './types';

function FlashList<T>({enableAnimatedKeyboardDismissal, onScroll: onScrollProp, inverted, ...restProps}: CustomFlashListProps<T>) {
const emitComposerScrollEvents = useEmitComposerScrollEvents({enabled: true, inverted});

const handleScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
Expand All @@ -15,6 +18,17 @@ function FlashList<T>({onScroll: onScrollProp, inverted, ...restProps}: FlashLis
emitComposerScrollEvents();
};

if (enableAnimatedKeyboardDismissal) {
return (
<KeyboardDismissibleFlashList
{...restProps}
inverted={inverted}
// Composer scroll events are emitted in `KeyboardDismissibleFlatList` separately, therefore we pass the `onScroll` prop instead of the `handleScroll` callback.
onScroll={onScrollProp}
/>
);
}

return (
<ShopifyFlashList<T>
{...restProps}
Expand Down
38 changes: 36 additions & 2 deletions src/components/FlashList/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,41 @@
import type {FlashListProps, FlashListRef} from '@shopify/flash-list';
import type {RefObject} from 'react';
import type {FlatList} from 'react-native';
import type {CellRendererProps, FlatList} from 'react-native';

/** Ref to the underlying list instance attached via `ref={}`. */
type FlatListRefType = RefObject<FlatList<unknown> | null> | null;
type FlatListRefType<T = unknown> = RefObject<FlatList<T> | null> | null;

/** Ref to the underlying list instance attached via `ref={}`. */
type FlashListRefType<T = unknown> = RefObject<FlashListRef<T> | null> | null;

/** Ref shape shared by report lists and the report scroll manager. */
type ActionListRefType = RefObject<{
getNativeScrollRef?: () => unknown;
scrollToEnd: (params?: {animated?: boolean}) => void;
scrollToIndex: (params: {animated?: boolean; index: number; viewOffset?: number; viewPosition?: number}) => void;
scrollToOffset: (params: {animated?: boolean; offset: number}) => void;
} | null> | null;

type CustomFlashListProps<T> = Omit<FlashListProps<T>, 'CellRendererComponent'> & {
/** Ref to the underlying list instance. */
ref?: FlashListRefType<T>;

/**
* Whether to use the animated keyboard handler capabilities on native (iOS and Android)
* Allows for interactive keyboard dismissal when the user drags the keyboard down
*/
enableAnimatedKeyboardDismissal?: boolean;

/**
* Custom cell renderer component
*/
CellRendererComponent?: React.ComponentType<CellRendererProps<T>>;

/** The array of items to render in the list. */
data: T[];

/** Function that extracts a unique key for each item in the list. */
keyExtractor: (item: T, index: number) => string;
};
export default FlatListRefType;
export type {ActionListRefType, FlashListRefType, CustomFlashListProps};
8 changes: 4 additions & 4 deletions src/components/FlatList/FlatList/index.android.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type {CustomFlatListProps} from './types';

// FlatList wrapped with the freeze component will lose its scroll state when frozen (only for Android).
// CustomFlatList saves the offset and use it for scrollToOffset() when unfrozen.
function CustomFlatList<T>({ref, enableAnimatedKeyboardDismissal = false, onMomentumScrollEnd, onScroll, shouldHideContent = false, ...props}: CustomFlatListProps<T>) {
function CustomFlatList<T>({ref, enableAnimatedKeyboardDismissal = false, onMomentumScrollEnd, onScroll, shouldHideContent = false, ...restProps}: CustomFlatListProps<T>) {
const lastScrollOffsetRef = useRef(0);
const styles = useThemeStyles();

Expand Down Expand Up @@ -47,12 +47,12 @@ function CustomFlatList<T>({ref, enableAnimatedKeyboardDismissal = false, onMome
}, [onScreenFocus]),
);

const contentContainerStyle = [props.contentContainerStyle, shouldHideContent && styles.opacity0];
const contentContainerStyle = [restProps.contentContainerStyle, shouldHideContent && styles.opacity0];

if (enableAnimatedKeyboardDismissal) {
return (
<KeyboardDismissibleFlatList
{...props}
{...restProps}
ref={ref}
onScroll={handleScroll}
onMomentumScrollEnd={handleScrollEnd}
Expand All @@ -63,7 +63,7 @@ function CustomFlatList<T>({ref, enableAnimatedKeyboardDismissal = false, onMome

return (
<FlatList<T>
{...props}
{...restProps}
ref={ref}
onScroll={handleScroll}
onMomentumScrollEnd={handleScrollEnd}
Expand Down
12 changes: 1 addition & 11 deletions src/components/FlatList/FlatList/index.ios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import useFlatListHandle from '@components/FlatList/hooks/useFlatListHandle';
import type {FlatListInnerRefType} from '@components/FlatList/hooks/useFlatListHandle';
import KeyboardDismissibleFlatList from '@components/KeyboardDismissibleFlatList';

import useEmitComposerScrollEvents from '@hooks/useEmitComposerScrollEvents';
import useThemeStyles from '@hooks/useThemeStyles';

import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native';
Expand Down Expand Up @@ -42,15 +41,6 @@ function CustomFlatList<T>({
[onMomentumScrollEnd],
);

const emitComposerScrollEvents = useEmitComposerScrollEvents({enabled: !enableAnimatedKeyboardDismissal, inverted: restProps.inverted});
const handleScroll = useCallback(
(e: NativeSyntheticEvent<NativeScrollEvent>) => {
onScrollProp?.(e);
emitComposerScrollEvents();
},
[emitComposerScrollEvents, onScrollProp],
);

const listRef = useRef<FlatListInnerRefType<T> | null>(null);
useFlatListHandle<T>({
ref,
Expand Down Expand Up @@ -84,7 +74,7 @@ function CustomFlatList<T>({
{...restProps}
ref={listRef}
maintainVisibleContentPosition={maintainVisibleContentPosition}
onScroll={handleScroll}
onScroll={onScrollProp}
onMomentumScrollBegin={handleScrollBegin}
onMomentumScrollEnd={handleScrollEnd}
contentContainerStyle={contentContainerStyle}
Expand Down
Loading
Loading