Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
4af5484
rebase against main
borys3kk Jul 6, 2026
198ee8e
merge main
borys3kk Jul 6, 2026
1099740
Resolve self-review findings for the expense-added growl wiring
borys3kk Jul 7, 2026
b8da096
Address second self-review pass on growl wiring
borys3kk Jul 7, 2026
6105bcf
merge main
borys3kk Jul 7, 2026
5a77400
fix knip
borys3kk Jul 7, 2026
ecb42ae
Fix growl fast path and Share flow post-create feedback
borys3kk Jul 8, 2026
134d827
fix share feedback consistent
borys3kk Jul 9, 2026
917abe5
Address review feedback on growl wiring
borys3kk Jul 9, 2026
6fe6ec5
Address code-review pass on growl UX edge cases
borys3kk Jul 13, 2026
3d0bcd2
merge main
borys3kk Jul 13, 2026
ba3ad4b
Merge branch 'main' of github.com:Expensify/App into borys3kk-introdu…
borys3kk Jul 13, 2026
3dc2fe1
Remove the isLastTransactionOfBatch prop
mhawryluk Jul 13, 2026
348ca76
Add missing jsdoc comments to type properties
mhawryluk Jul 13, 2026
3e03816
Remove Onyx.connectWithoutView usages that can be removed
mhawryluk Jul 14, 2026
db8bc4d
Merge branch 'main' into borys3kk-introduce-growls-pr2
mhawryluk Jul 15, 2026
6c817ed
Fix after merge
mhawryluk Jul 15, 2026
56c98ae
Move growl invocation away from actions and into UI
mhawryluk Jul 15, 2026
5f23f07
Fix tests
mhawryluk Jul 15, 2026
21434f2
Fix more tests
mhawryluk Jul 15, 2026
b92595b
Add runAfterDeferredWrite to remove the growl safety timeout
mhawryluk Jul 15, 2026
1a38576
Extract navigateToTransactionThread function and add tests for it
mhawryluk Jul 16, 2026
f116c13
Navigate to expense report for single-transaction case instead of /se…
mhawryluk Jul 16, 2026
97fdc55
Rename navigateToTransactionThread to navigateToCreatedExpense
mhawryluk Jul 16, 2026
b308b00
Remove the getReportTransactions fallback from showExpenseAddedGrowl
mhawryluk Jul 16, 2026
99a4af2
Replace deprecated allReports usage in showExpenseAddedGrowl with Ony…
mhawryluk Jul 16, 2026
da8e1a3
Remove unused scroll-related code
mhawryluk Jul 16, 2026
2989e8d
Bring back refetching group snapshot on new transactions
mhawryluk Jul 16, 2026
99763ae
Fix growl's "View" navigation for image share to selfDM
mhawryluk Jul 17, 2026
1f77a2f
Resolve transaction lazily in showExpenseAddedGrowl instead of passin…
mhawryluk Jul 17, 2026
06fc939
Show growl after splitting an expense
mhawryluk Jul 17, 2026
d95b0b6
Add a warning comment to runAfterDeferredWrite
mhawryluk Jul 17, 2026
b47d07a
Show growl after duplicating expenses
mhawryluk Jul 20, 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: 0 additions & 4 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -783,9 +783,6 @@ const ONYXKEYS = {
/** Whether the user has denied the contact import permission prompt */
HAS_DENIED_CONTACT_IMPORT_PROMPT: 'hasDeniedContactImportPrompt',

/** The transaction IDs to be highlighted when opening the Expenses search route page */
TRANSACTION_IDS_HIGHLIGHT_ON_SEARCH_ROUTE: 'transactionIdsHighlightOnSearchRoute',

/** The report ID to be highlighted when returning to the workspace rooms page */
ROOM_ID_HIGHLIGHT_ON_ROOMS_PAGE: 'roomIDHighlightOnRoomsPage',

Expand Down Expand Up @@ -1723,7 +1720,6 @@ type OnyxValuesMapping = {
[ONYXKEYS.NVP_REPORT_DETAILS_COLUMNS]: string[];
[ONYXKEYS.HAS_DENIED_CONTACT_IMPORT_PROMPT]: boolean | undefined;
[ONYXKEYS.PERSONAL_POLICY_ID]: string;
[ONYXKEYS.TRANSACTION_IDS_HIGHLIGHT_ON_SEARCH_ROUTE]: Record<string, Record<string, boolean>>;
[ONYXKEYS.ROOM_ID_HIGHLIGHT_ON_ROOMS_PAGE]: string | null;
[ONYXKEYS.DOMAIN_GROUP_CREATE_PREFERRED_POLICY_ID]: string | undefined;
};
Expand Down
11 changes: 10 additions & 1 deletion src/components/MoneyRequestHeaderSecondaryActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import initSplitExpense from '@libs/actions/SplitExpenses';
import {setNameValuePair} from '@libs/actions/User';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {getExistingTransactionID} from '@libs/IOUUtils';
import {surfaceExpenseCreatedFeedback} from '@libs/Navigation/helpers/navigateAfterExpenseCreate';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types';
import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList} from '@libs/Navigation/types';
Expand Down Expand Up @@ -243,11 +244,12 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money
const optimisticIOUReportID = generateReportID();
const activePolicyCategoriesMap = defaultPolicyCategories ?? {};

let lastDuplicate: {iouReportID?: string; transactionID?: string} | undefined;
for (const item of transactions) {
const existingTransactionID = getExistingTransactionID(item.linkedTrackedExpenseReportAction);
const existingTransactionDraft = existingTransactionID ? transactionDrafts?.[existingTransactionID] : undefined;

duplicateTransactionAction({
const result = duplicateTransactionAction({
transaction: item,
optimisticChatReportID,
optimisticIOUReportID,
Expand All @@ -271,6 +273,13 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money
delegateAccountID,
policyTagList,
});
if (result?.transactionID) {
lastDuplicate = {iouReportID: result.iouReport?.reportID, transactionID: result.transactionID};
}
}

if (lastDuplicate?.transactionID) {
surfaceExpenseCreatedFeedback(lastDuplicate);
}
};

Expand Down
8 changes: 1 addition & 7 deletions src/components/Search/ChatSearchView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import CONST from '@src/CONST';

import type {NativeSyntheticEvent} from 'react-native';

import React, {useImperativeHandle} from 'react';
import React from 'react';

import type {SearchListItem} from './SearchList/ListItem/types';
import type {CommonSearchViewProps} from './searchViewProps';
Expand Down Expand Up @@ -41,15 +41,13 @@ function ChatSearchView({
SearchTableHeader: searchTableHeader,
tableHeaderVisible,
hasLoadedAllTransactions,
newTransactions,
onSelectRow,
ListFooterComponent,
onEndReached,
onLayout,
onScroll,
contentContainerStyle,
containerStyle,
ref,
}: ChatSearchViewProps) {
const {type} = queryJSON;

Expand All @@ -65,9 +63,6 @@ function ChatSearchView({
const selectedItemsLength = data.reduce((acc, item) => acc + (item.keyForList && selectedTransactions[item.keyForList]?.isSelected ? 1 : 0), 0);
const totalItems = data.filter((item) => !isRowDeleted(item)).length;

// Flat data maps 1:1 to the rendered list, so highlight-scroll-to-index is the same as scroll-to-data-index.
useImperativeHandle(ref, () => ({scrollToIndex: scrollToListIndex}), [scrollToListIndex]);

const renderItem = (item: SearchListItem, index: number, isItemFocused: boolean, onFocus?: (event: NativeSyntheticEvent<ExtendedTargetedEvent>) => void) => (
// Chat rows never animate their exit (only grouped expenses do), so the wrapper just preserves the overflow clip.
<AnimatedExitRow
Expand Down Expand Up @@ -132,7 +127,6 @@ function ChatSearchView({
ListFooterComponent={ListFooterComponent}
onLayout={onLayout}
contentContainerStyle={contentContainerStyle}
newTransactions={newTransactions}
/>
{modal}
</SearchListViewLayout>
Expand Down
11 changes: 2 additions & 9 deletions src/components/Search/ExpenseFlatSearchView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import CONST from '@src/CONST';

import type {NativeSyntheticEvent} from 'react-native';

import React, {useImperativeHandle} from 'react';
import React from 'react';

import type {SearchListItem} from './SearchList/ListItem/types';
import type {CommonSearchViewProps, TransactionViewExtras} from './searchViewProps';
Expand Down Expand Up @@ -32,8 +32,7 @@ const isRowDeleted = (item: SearchListItem) => item.pendingAction === CONST.RED_
* renderer, single-pass visibility/selection counts, and the highlight-scroll imperative handle.
* `TransactionListItem` is the only row renderer here and rows always animate, so the
* group/sticky/chat/task branches of `SearchList` do not apply. Keyboard navigation is inherited from
* `BaseSearchList`; the post-create highlight stays in the router (the snapshot stamps
* `shouldAnimateInHighlight`, and `newTransactions` flows into `extraData`).
* `BaseSearchList`.
*/
function ExpenseFlatSearchView({
queryJSON,
Expand All @@ -47,15 +46,13 @@ function ExpenseFlatSearchView({
SearchTableHeader: searchTableHeader,
tableHeaderVisible,
hasLoadedAllTransactions,
newTransactions,
onSelectRow,
ListFooterComponent,
onEndReached,
onLayout,
onScroll,
contentContainerStyle,
containerStyle,
ref,
}: ExpenseFlatSearchViewProps) {
const {type} = queryJSON;

Expand Down Expand Up @@ -89,9 +86,6 @@ function ExpenseFlatSearchView({
const selectedItemsLength = data.reduce((acc, item) => acc + (item.keyForList && selectedTransactions[item.keyForList]?.isSelected ? 1 : 0), 0);
const totalItems = data.filter((item) => !isRowDeleted(item)).length;

// Flat data maps 1:1 to the rendered list, so highlight-scroll-to-index is the same as scroll-to-data-index.
useImperativeHandle(ref, () => ({scrollToIndex: scrollToListIndex}), [scrollToListIndex]);

const renderItem = (item: SearchListItem, index: number, isItemFocused: boolean, onFocus?: (event: NativeSyntheticEvent<ExtendedTargetedEvent>) => void) => {
const isDisabled = isRowDeleted(item);
// Only expense row exits animate; invoice and trip transaction lists do not (matches the legacy per-type gate).
Expand Down Expand Up @@ -171,7 +165,6 @@ function ExpenseFlatSearchView({
ListFooterComponent={ListFooterComponent}
onLayout={onLayout}
contentContainerStyle={contentContainerStyle}
newTransactions={newTransactions}
isAttendeesEnabledForMovingPolicy={isAttendeesEnabledForMovingPolicy}
nonPersonalAndWorkspaceCards={nonPersonalAndWorkspaceCards}
/>
Expand Down
26 changes: 2 additions & 24 deletions src/components/Search/ExpenseGroupedSearchView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type {Transaction} from '@src/types/onyx';

import type {NativeSyntheticEvent} from 'react-native';

import React, {useImperativeHandle, useState} from 'react';
import React, {useState} from 'react';

import type {SearchListItem} from './SearchList/ListItem/types';
import type {CommonSearchViewProps, TransactionViewExtras} from './searchViewProps';
Expand Down Expand Up @@ -65,7 +65,7 @@ function buildSplitGroupData(data: SearchListItem[], shouldSplitGroups: boolean)
};
}

/** Maps each group's `keyForList` to the id of a just-created transaction inside it, for the highlight animation. */
/** Maps each group's `keyForList` to the id of a newly-added transaction inside it, used to refetch the group's child snapshot. */
function buildNewTransactionIDMap(data: SearchListItem[], newTransactions: Transaction[], groupBy: SearchQueryJSON['groupBy']) {
const map = new Map<string, string>();
if (newTransactions.length === 0) {
Expand Down Expand Up @@ -110,7 +110,6 @@ function ExpenseGroupedSearchView({
onScroll,
contentContainerStyle,
containerStyle,
ref,
}: ExpenseGroupedSearchViewProps) {
const {type, groupBy} = queryJSON;
const {isLargeScreenWidth} = useResponsiveLayout();
Expand Down Expand Up @@ -173,26 +172,6 @@ function ExpenseGroupedSearchView({
const firstVisibleIndex = listData.findIndex(isItemVisible);
const lastVisibleIndex = listData.findLastIndex(isItemVisible);

// The router highlights by source-data index; remap it to the split-list index before scrolling.
useImperativeHandle(
ref,
() => ({
scrollToIndex: (index: number, animated = true) => {
if (!shouldSplit) {
scrollToListIndex(index, animated);
return;
}
const item = data.at(index);
if (!item) {
return;
}
const splitIndex = item.keyForList ? listData.findIndex((listItem) => listItem.keyForList === `header_${item.keyForList}`) : -1;
scrollToListIndex(splitIndex !== -1 ? splitIndex : index, animated);
},
}),
[data, listData, shouldSplit, scrollToListIndex],
);

const getItemType = (item: SearchListItem) => {
if (!shouldSplit) {
return undefined;
Expand Down Expand Up @@ -347,7 +326,6 @@ function ExpenseGroupedSearchView({
ListFooterComponent={ListFooterComponent}
onLayout={onLayout}
contentContainerStyle={contentContainerStyle}
newTransactions={newTransactions}
isAttendeesEnabledForMovingPolicy={isAttendeesEnabledForMovingPolicy}
nonPersonalAndWorkspaceCards={nonPersonalAndWorkspaceCards}
stickyHeaderIndices={stickyHeaderIndices}
Expand Down
8 changes: 1 addition & 7 deletions src/components/Search/ExpenseReportSearchView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import CONST from '@src/CONST';

import type {NativeSyntheticEvent} from 'react-native';

import React, {useImperativeHandle} from 'react';
import React from 'react';

import type {SearchListItem} from './SearchList/ListItem/types';
import type {CommonSearchViewProps} from './searchViewProps';
Expand Down Expand Up @@ -47,15 +47,13 @@ function ExpenseReportSearchView({
SearchTableHeader: searchTableHeader,
tableHeaderVisible,
hasLoadedAllTransactions,
newTransactions,
onSelectRow,
ListFooterComponent,
onEndReached,
onLayout,
onScroll,
contentContainerStyle,
containerStyle,
ref,
}: ExpenseReportSearchViewProps) {
const {type} = queryJSON;

Expand Down Expand Up @@ -92,9 +90,6 @@ function ExpenseReportSearchView({
emptyReports.reduce((acc, item) => acc + (isRowSelected(item.keyForList, selectedTransactions) ? 1 : 0), 0);
const totalItems = flattenedTransactions.filter((item) => !isRowDeleted(item)).length + emptyReports.filter((item) => !isRowDeleted(item)).length;

// Report data maps 1:1 to the rendered list, so highlight-scroll-to-index is the same as scroll-to-data-index.
useImperativeHandle(ref, () => ({scrollToIndex: scrollToListIndex}), [scrollToListIndex]);

const renderItem = (item: SearchListItem, index: number, isItemFocused: boolean, onFocus?: (event: NativeSyntheticEvent<ExtendedTargetedEvent>) => void) => (
// Report rows never animate their exit (only grouped expenses do), so the wrapper just preserves the overflow clip.
<AnimatedExitRow
Expand Down Expand Up @@ -167,7 +162,6 @@ function ExpenseReportSearchView({
ListFooterComponent={ListFooterComponent}
onLayout={onLayout}
contentContainerStyle={contentContainerStyle}
newTransactions={newTransactions}
/>
{modal}
</SearchListViewLayout>
Expand Down
5 changes: 2 additions & 3 deletions src/components/Search/SearchList/BaseSearchList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ function BaseSearchList({
onLayout,
contentContainerStyle,
flattenedItemsLength,
newTransactions,
isAttendeesEnabledForMovingPolicy,
nonPersonalAndWorkspaceCards,
stickyHeaderIndices,
Expand Down Expand Up @@ -171,8 +170,8 @@ function BaseSearchList({
}, [setHasKeyBeenPressed]);

const extraData = useMemo(
() => [focusedIndex, columns, newTransactions, nonPersonalAndWorkspaceCards, isAttendeesEnabledForMovingPolicy],
[focusedIndex, columns, newTransactions, nonPersonalAndWorkspaceCards, isAttendeesEnabledForMovingPolicy],
() => [focusedIndex, columns, nonPersonalAndWorkspaceCards, isAttendeesEnabledForMovingPolicy],
[focusedIndex, columns, nonPersonalAndWorkspaceCards, isAttendeesEnabledForMovingPolicy],
);

return (
Expand Down
5 changes: 1 addition & 4 deletions src/components/Search/SearchList/BaseSearchList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type {SearchListItem} from '@components/Search/SearchList/ListItem/types'
import type {SearchColumnType} from '@components/Search/types';
import type {ExtendedTargetedEvent} from '@components/SelectionList/ListItem/types';

import type {CardList, Transaction} from '@src/types/onyx';
import type {CardList} from '@src/types/onyx';

import type {FlashListProps, FlashListRef} from '@shopify/flash-list';
import type {RefObject} from 'react';
Expand Down Expand Up @@ -32,9 +32,6 @@ type BaseSearchListProps = Pick<
/** The columns that might change to trigger re-render via extraData */
columns: SearchColumnType[];

/** The transactions that might trigger re-render via extraData */
newTransactions: Transaction[];

/** The length of the flattened items in the list */
flattenedItemsLength: number;

Expand Down
8 changes: 1 addition & 7 deletions src/components/Search/TaskSearchView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import CONST from '@src/CONST';

import type {NativeSyntheticEvent} from 'react-native';

import React, {useImperativeHandle} from 'react';
import React from 'react';

import type {SearchListItem} from './SearchList/ListItem/types';
import type {CommonSearchViewProps} from './searchViewProps';
Expand Down Expand Up @@ -40,15 +40,13 @@ function TaskSearchView({
SearchTableHeader: searchTableHeader,
tableHeaderVisible,
hasLoadedAllTransactions,
newTransactions,
onSelectRow,
ListFooterComponent,
onEndReached,
onLayout,
onScroll,
contentContainerStyle,
containerStyle,
ref,
}: TaskSearchViewProps) {
const {type} = queryJSON;

Expand All @@ -62,9 +60,6 @@ function TaskSearchView({
const selectedItemsLength = data.reduce((acc, item) => acc + (item.keyForList && selectedTransactions[item.keyForList]?.isSelected ? 1 : 0), 0);
const totalItems = data.filter((item) => !isRowDeleted(item)).length;

// Flat data maps 1:1 to the rendered list, so highlight-scroll-to-index is the same as scroll-to-data-index.
useImperativeHandle(ref, () => ({scrollToIndex: scrollToListIndex}), [scrollToListIndex]);

const renderItem = (item: SearchListItem, index: number, isItemFocused: boolean, onFocus?: (event: NativeSyntheticEvent<ExtendedTargetedEvent>) => void) => (
// Task rows never animate their exit (only grouped expenses do), so the wrapper just preserves the overflow clip.
<AnimatedExitRow
Expand Down Expand Up @@ -130,7 +125,6 @@ function TaskSearchView({
ListFooterComponent={ListFooterComponent}
onLayout={onLayout}
contentContainerStyle={contentContainerStyle}
newTransactions={newTransactions}
/>
{modal}
</SearchListViewLayout>
Expand Down
Loading
Loading