Skip to content

Commit 32abd07

Browse files
authored
Merge pull request Expensify#63623 from nkdengineer/fix/55211
feat: update Search/Reports page immediately following report actions
2 parents fb0c6a9 + 9f15c43 commit 32abd07

6 files changed

Lines changed: 273 additions & 127 deletions

File tree

patches/react-native-reanimated/details.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,10 @@
1515
- E/App issue: 🛑
1616
- PR Introducing Patch: [NR 0.75 upgrade](https://github.com/Expensify/App/pull/45289)
1717

18+
### [react-native-reanimated+3.19.1+003+correctly-handle-Easing.bezier.patch](react-native-reanimated+3.19.1+003+correctly-handle-Easing.bezier.patch)
19+
20+
- Reason: The Easing.bezier animation doesn't work on web
21+
- Upstream PR/issue: https://github.com/software-mansion/react-native-reanimated/pull/8049
22+
- E/App issue: https://github.com/Expensify/App/pull/63623
23+
- PR Introducing Patch: 🛑
24+
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
diff --git a/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/Easing.web.js b/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/Easing.web.js
2+
index ed0f9d3..6baf136 100644
3+
--- a/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/Easing.web.js
4+
+++ b/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/Easing.web.js
5+
@@ -14,4 +14,18 @@ export const WebEasings = {
6+
export function getEasingByName(easingName) {
7+
return `cubic-bezier(${WebEasings[easingName].toString()})`;
8+
}
9+
-//# sourceMappingURL=Easing.web.js.map
10+
\ No newline at end of file
11+
+export function maybeGetBezierEasing(easing) {
12+
+ if (!('factory' in easing)) {
13+
+ return null;
14+
+ }
15+
+ const easingFactory = easing.factory;
16+
+ if (!('__closure' in easingFactory)) {
17+
+ return null;
18+
+ }
19+
+ const closure = easingFactory.__closure;
20+
+ if (!('Bezier' in closure)) {
21+
+ return null;
22+
+ }
23+
+ return `cubic-bezier(${closure.x1}, ${closure.y1}, ${closure.x2}, ${closure.y2})`;
24+
+}
25+
+
26+
diff --git a/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/componentUtils.js b/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/componentUtils.js
27+
index 7f724c4..ad53a74 100644
28+
--- a/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/componentUtils.js
29+
+++ b/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/componentUtils.js
30+
@@ -10,18 +10,28 @@ import { setElementPosition, snapshots } from "./componentStyle.js";
31+
import { Animations, TransitionType } from "./config.js";
32+
import { TransitionGenerator } from "./createAnimation.js";
33+
import { scheduleAnimationCleanup } from "./domUtils.js";
34+
-import { getEasingByName, WebEasings } from "./Easing.web.js";
35+
+import {
36+
+ getEasingByName,
37+
+ maybeGetBezierEasing,
38+
+ WebEasings
39+
+} from "./Easing.web.js";
40+
import { prepareCurvedTransition } from "./transition/Curved.web.js";
41+
function getEasingFromConfig(config) {
42+
if (!config.easingV) {
43+
return getEasingByName('linear');
44+
}
45+
const easingName = config.easingV[EasingNameSymbol];
46+
- if (!(easingName in WebEasings)) {
47+
- logger.warn(`Selected easing is not currently supported on web.`);
48+
+ if (easingName in WebEasings) {
49+
+ return getEasingByName(easingName);
50+
+ }
51+
+ const bezierEasing = maybeGetBezierEasing(config.easingV);
52+
+ if (!bezierEasing) {
53+
+ logger.warn(
54+
+ `Selected easing is not currently supported on web. Using linear easing instead.`
55+
+ );
56+
return getEasingByName('linear');
57+
}
58+
- return getEasingByName(easingName);
59+
+ return bezierEasing;
60+
}
61+
function getRandomDelay(maxDelay = 1000) {
62+
return Math.floor(Math.random() * (maxDelay + 1)) / 1000;

src/CONST/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6280,6 +6280,7 @@ const CONST = {
62806280

62816281
SEARCH: {
62826282
RESULTS_PAGE_SIZE: 50,
6283+
EXITING_ANIMATION_DURATION: 200,
62836284
DATA_TYPES: {
62846285
EXPENSE: 'expense',
62856286
INVOICE: 'invoice',

src/components/Search/SearchList.tsx

Lines changed: 62 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import React, {forwardRef, useCallback, useContext, useEffect, useImperativeHand
55
import type {ForwardedRef} from 'react';
66
import {View} from 'react-native';
77
import type {NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native';
8-
import Animated from 'react-native-reanimated';
8+
import Animated, {Easing, FadeOutUp, LinearTransition} from 'react-native-reanimated';
99
import Checkbox from '@components/Checkbox';
1010
import * as Expensicons from '@components/Icon/Expensicons';
1111
import MenuItem from '@components/MenuItem';
@@ -25,6 +25,7 @@ import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
2525
import useKeyboardState from '@hooks/useKeyboardState';
2626
import useLocalize from '@hooks/useLocalize';
2727
import useOnyx from '@hooks/useOnyx';
28+
import usePrevious from '@hooks/usePrevious';
2829
import useResponsiveLayout from '@hooks/useResponsiveLayout';
2930
import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
3031
import useThemeStyles from '@hooks/useThemeStyles';
@@ -39,6 +40,8 @@ import {createItemHeightCalculator} from './itemHeightCalculator';
3940
import ITEM_HEIGHTS from './itemHeights';
4041
import type {SearchQueryJSON} from './types';
4142

43+
const easing = Easing.bezier(0.76, 0.0, 0.24, 1.0);
44+
4245
const AnimatedFlashListComponent = Animated.createAnimatedComponent(FlashList<SearchListItem>);
4346

4447
type SearchListItem = TransactionListItemType | TransactionGroupListItemType | ReportActionListItemType | TaskListItemType;
@@ -78,6 +81,9 @@ type SearchListProps = Pick<FlashListProps<SearchListItem>, 'onScroll' | 'conten
7881
/** Whether to prevent long press of options */
7982
shouldPreventLongPressRow?: boolean;
8083

84+
/** Whether to animate the items in the list */
85+
shouldAnimate?: boolean;
86+
8187
/** The search query */
8288
queryJSON: SearchQueryJSON;
8389

@@ -127,6 +133,7 @@ function SearchList(
127133
queryJSON,
128134
onViewableItemsChanged,
129135
onLayout,
136+
shouldAnimate,
130137
estimatedItemSize = ITEM_HEIGHTS.NARROW_WITHOUT_DRAWER.STANDARD,
131138
isMobileSelectionModeEnabled,
132139
}: SearchListProps,
@@ -163,6 +170,7 @@ function SearchList(
163170
const itemFocusTimeoutRef = useRef<NodeJS.Timeout | null>(null);
164171
const {isKeyboardShown} = useKeyboardState();
165172
const {safeAreaPaddingBottomStyle} = useSafeAreaPaddings();
173+
const prevDataLength = usePrevious(data.length);
166174
// We need to use isSmallScreenWidth instead of shouldUseNarrowLayout here because there is a race condition that causes shouldUseNarrowLayout to change indefinitely in this component
167175
// See https://github.com/Expensify/App/issues/48675 for more details
168176
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
@@ -177,6 +185,7 @@ function SearchList(
177185

178186
const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: false});
179187

188+
const hasItemsBeingRemoved = prevDataLength && prevDataLength > data.length;
180189
const personalDetails = usePersonalDetails();
181190

182191
const [userWalletTierName] = useOnyx(ONYXKEYS.USER_WALLET, {selector: (wallet) => wallet?.tierName, canBeMissing: false});
@@ -316,62 +325,72 @@ function SearchList(
316325
const isDisabled = item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
317326

318327
return (
319-
<ListItem
320-
showTooltip
321-
isFocused={isItemFocused}
322-
onSelectRow={onSelectRow}
323-
onFocus={(event: NativeSyntheticEvent<ExtendedTargetedEvent>) => {
324-
// Prevent unexpected scrolling on mobile Chrome after the context menu closes by ignoring programmatic focus not triggered by direct user interaction.
325-
if (isMobileChrome() && event.nativeEvent) {
326-
if (!event.nativeEvent.sourceCapabilities) {
327-
return;
328-
}
329-
// Ignore the focus if it's caused by a touch event on mobile chrome.
330-
// For example, a long press will trigger a focus event on mobile chrome
331-
if (event.nativeEvent.sourceCapabilities.firesTouchEvents) {
332-
return;
328+
<Animated.View
329+
exiting={shouldAnimate ? FadeOutUp.duration(CONST.SEARCH.EXITING_ANIMATION_DURATION).easing(easing) : undefined}
330+
entering={undefined}
331+
style={styles.overflowHidden}
332+
layout={shouldAnimate && hasItemsBeingRemoved ? LinearTransition.easing(easing).duration(CONST.SEARCH.EXITING_ANIMATION_DURATION) : undefined}
333+
>
334+
<ListItem
335+
showTooltip
336+
isFocused={isItemFocused}
337+
onSelectRow={onSelectRow}
338+
onFocus={(event: NativeSyntheticEvent<ExtendedTargetedEvent>) => {
339+
// Prevent unexpected scrolling on mobile Chrome after the context menu closes by ignoring programmatic focus not triggered by direct user interaction.
340+
if (isMobileChrome() && event.nativeEvent) {
341+
if (!event.nativeEvent.sourceCapabilities) {
342+
return;
343+
}
344+
// Ignore the focus if it's caused by a touch event on mobile chrome.
345+
// For example, a long press will trigger a focus event on mobile chrome
346+
if (event.nativeEvent.sourceCapabilities.firesTouchEvents) {
347+
return;
348+
}
333349
}
334-
}
335-
setFocusedIndex(index);
336-
}}
337-
onLongPressRow={handleLongPressRow}
338-
onCheckboxPress={onCheckboxPress}
339-
canSelectMultiple={canSelectMultiple}
340-
item={{
341-
shouldAnimateInHighlight: isItemHighlighted,
342-
...item,
343-
}}
344-
shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow}
345-
queryJSONHash={hash}
346-
policies={policies}
347-
isDisabled={isDisabled}
348-
allReports={allReports}
349-
groupBy={groupBy}
350-
userWalletTierName={userWalletTierName}
351-
isUserValidated={isUserValidated}
352-
personalDetails={personalDetails}
353-
userBillingFundID={userBillingFundID}
354-
/>
350+
setFocusedIndex(index);
351+
}}
352+
onLongPressRow={handleLongPressRow}
353+
onCheckboxPress={onCheckboxPress}
354+
canSelectMultiple={canSelectMultiple}
355+
item={{
356+
shouldAnimateInHighlight: isItemHighlighted,
357+
...item,
358+
}}
359+
shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow}
360+
queryJSONHash={hash}
361+
policies={policies}
362+
isDisabled={isDisabled}
363+
allReports={allReports}
364+
groupBy={groupBy}
365+
userWalletTierName={userWalletTierName}
366+
isUserValidated={isUserValidated}
367+
personalDetails={personalDetails}
368+
userBillingFundID={userBillingFundID}
369+
/>
370+
</Animated.View>
355371
);
356372
},
357373
[
358-
ListItem,
359-
canSelectMultiple,
360374
focusedIndex,
361375
itemsToHighlight,
376+
shouldAnimate,
377+
styles.overflowHidden,
378+
hasItemsBeingRemoved,
379+
ListItem,
380+
onSelectRow,
362381
handleLongPressRow,
363382
onCheckboxPress,
364-
onSelectRow,
365-
policies,
366-
hash,
367-
groupBy,
368-
setFocusedIndex,
383+
canSelectMultiple,
369384
shouldPreventDefaultFocusOnSelectRow,
385+
hash,
386+
policies,
370387
allReports,
388+
groupBy,
371389
userWalletTierName,
372390
isUserValidated,
373391
personalDetails,
374392
userBillingFundID,
393+
setFocusedIndex,
375394
],
376395
);
377396

src/components/Search/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS
813813
onViewableItemsChanged={onViewableItemsChanged}
814814
onLayout={onLayout}
815815
isMobileSelectionModeEnabled={isMobileSelectionModeEnabled}
816+
shouldAnimate={type === CONST.SEARCH.DATA_TYPES.EXPENSE}
816817
/>
817818
</SearchScopeProvider>
818819
);

0 commit comments

Comments
 (0)