Skip to content
Merged
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
d1466c6
Store the currently opened report in Onyx
youssef-lr Mar 23, 2026
42b4e33
Rename stuff. And unset the active reportID when unmounting.
youssef-lr Mar 23, 2026
b7dae0a
Remove unnecessary effect
youssef-lr Mar 23, 2026
0251056
Add activeReportID to addComment call
youssef-lr Mar 24, 2026
032aa07
Avoid overwriting activeReportID with Concierge's ID. Add comments.
youssef-lr Mar 24, 2026
1fa59f3
Merge branch 'main' into youssef_concierge_anywhere_report_context
youssef-lr Mar 26, 2026
29ce804
Update naming
youssef-lr Mar 27, 2026
7e4fd1e
Use currentReportID or currentRHPReportID from navigation to set side…
youssef-lr Mar 28, 2026
bcac18c
Cleanup
youssef-lr Mar 28, 2026
619dd4d
JSON stringify sidePanelContext
youssef-lr Mar 28, 2026
6df8e49
Cleanup
youssef-lr Mar 28, 2026
53b3791
Fix Lint & TS
youssef-lr Mar 28, 2026
fe39d1d
Merge branch 'main' into youssef_concierge_anywhere_report_context
youssef-lr Apr 3, 2026
46fe425
Fix TS
youssef-lr Apr 3, 2026
144a97c
Merge branch 'main' into youssef_concierge_anywhere_report_context
youssef-lr Apr 7, 2026
c8140b0
Merge branch 'main' into youssef_concierge_anywhere_report_context
youssef-lr Apr 13, 2026
5bd7620
Include selected transactions or reports in context
youssef-lr Apr 13, 2026
aefa644
Send IDs as a string
youssef-lr Apr 14, 2026
9cdd521
Merge branch 'main' into youssef_concierge_anywhere_report_context
youssef-lr Apr 14, 2026
892adc9
Extract logic into hook
youssef-lr Apr 15, 2026
01e4cf1
Address comment
youssef-lr Apr 15, 2026
9fba131
Tidy up code
youssef-lr Apr 15, 2026
0187eb0
Send either selectedReportIDs or selectedTransactionIDs from the Repo…
youssef-lr Apr 15, 2026
9c88b74
Bug fix
youssef-lr Apr 15, 2026
1fc9ec7
Fix some edge cases
youssef-lr Apr 15, 2026
3d2450a
Address comments
youssef-lr Apr 16, 2026
eaf5d99
Add test
youssef-lr Apr 16, 2026
36087f0
Merge branch 'main' into youssef_concierge_anywhere_report_context
youssef-lr Apr 20, 2026
8889f68
Merge branch 'main' into youssef_concierge_anywhere_report_context
youssef-lr Apr 21, 2026
f94b8d7
Fix TS
youssef-lr Apr 21, 2026
baa3349
Merge branch 'main' into youssef_concierge_anywhere_report_context
youssef-lr Apr 21, 2026
754b5d9
Merge branch 'main' into youssef_concierge_anywhere_report_context
youssef-lr Apr 24, 2026
4b3b2a4
Merge branch 'main' into youssef_concierge_anywhere_report_context
youssef-lr Apr 27, 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
67 changes: 47 additions & 20 deletions src/hooks/useCurrentReportID.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type {NavigationState} from '@react-navigation/native';
import type {NavigationState, PartialState} from '@react-navigation/native';
import React, {createContext, startTransition, useCallback, useContext, useMemo, useRef, useState} from 'react';
import Navigation from '@libs/Navigation/Navigation';
import NAVIGATORS from '@src/NAVIGATORS';

type CurrentReportIDStateContextType = {
currentReportID: string | undefined;
currentRHPReportID?: string | undefined;
};

type CurrentReportIDActionsContextType = {
Expand All @@ -20,28 +22,46 @@ type CurrentReportIDContextProviderProps = {
onSetCurrentReportID?: (reportID: string | undefined) => void;
};

/**
* Traverse the focused route at each level of the navigation state to find a reportID param.
* This handles modal navigators (e.g. RightModalNavigator > ExpenseReport) that carry a reportID
* in their screen params but are not part of the ReportsSplitNavigator hierarchy.
*/
function getFocusedRouteReportID(state: NavigationState | PartialState<NavigationState>): string | undefined {
const index = state.index ?? state.routes.length - 1;
const focusedRoute = state.routes[index];
if (!focusedRoute) {
return;
}
if (focusedRoute.params && 'reportID' in focusedRoute.params && typeof focusedRoute.params.reportID === 'string') {
return focusedRoute.params.reportID;
}
if (focusedRoute.state) {
return getFocusedRouteReportID(focusedRoute.state);
}
}

const defaultCurrentReportIDActionsContext: CurrentReportIDActionsContextType = {
updateCurrentReportID: () => {},
};

const CurrentReportIDStateContext = createContext<CurrentReportIDStateContextType>({currentReportID: undefined});
const CurrentReportIDStateContext = createContext<CurrentReportIDStateContextType>({currentReportID: undefined, currentRHPReportID: undefined});

const CurrentReportIDActionsContext = createContext<CurrentReportIDActionsContextType>(defaultCurrentReportIDActionsContext);

function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderProps) {
const [currentReportID, setCurrentReportID] = useState<string | undefined>('');
const [currentRHPReportID, setCurrentRHPReportID] = useState<string | undefined>(undefined);
// Tracks the most recently requested reportID synchronously so the dedupe
// check below stays accurate even while a startTransition is pending.
const pendingReportIDRef = useRef<string | undefined>('');

/**
* This function is used to update the currentReportID
* This function is used to update the currentReportID and currentRHPReportID
* @param state root navigation state
*/
const updateCurrentReportID = useCallback(
(state: NavigationState) => {
const reportID = Navigation.getTopmostReportId(state);

/*
* Make sure we don't make the reportID undefined when switching between the chat list and settings tab.
* This helps prevent unnecessary re-renders.
Expand All @@ -50,25 +70,31 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro
if (params && 'screen' in params && typeof params.screen === 'string' && params.screen.indexOf('Settings_') !== -1) {
return;
}
if (pendingReportIDRef.current === reportID) {
return;
}

if (!pendingReportIDRef.current && !reportID) {
return;
const reportID = Navigation.getTopmostReportId(state);

if (pendingReportIDRef.current !== reportID) {
if (pendingReportIDRef.current || reportID) {
pendingReportIDRef.current = reportID;
props.onSetCurrentReportID?.(reportID);
// Mark the report ID update as a non-urgent transition so React can keep the
// UI responsive to user input while the (potentially expensive) report screen
// re-render is processed in the background.
startTransition(() => {
setCurrentReportID(reportID);
});
}
}

pendingReportIDRef.current = reportID;
props.onSetCurrentReportID?.(reportID);
// Mark the report ID update as a non-urgent transition so React can keep the
// UI responsive to user input while the (potentially expensive) report screen
// re-render is processed in the background.
startTransition(() => {
setCurrentReportID(reportID);
});
const focusedTopRoute = state.routes[state.index];
const modalReportID = focusedTopRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && focusedTopRoute.state ? getFocusedRouteReportID(focusedTopRoute.state) : undefined;

if (currentRHPReportID !== modalReportID && (currentRHPReportID || modalReportID)) {
setCurrentRHPReportID(modalReportID);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to re-render when onSetCurrentReportID changes
[setCurrentReportID],
[setCurrentReportID, setCurrentRHPReportID, currentRHPReportID],
);

const actionsContextValue = useMemo<CurrentReportIDActionsContextType>(
Expand All @@ -81,8 +107,9 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro
const stateContextValue = useMemo<CurrentReportIDStateContextType>(
() => ({
currentReportID,
currentRHPReportID,
}),
[currentReportID],
[currentReportID, currentRHPReportID],
);

return (
Expand Down
1 change: 1 addition & 0 deletions src/libs/API/parameters/AddCommentOrAttachmentParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type AddCommentOrAttachmentParams = {
pageHTML?: string;
optimisticConciergeReportActionID?: string;
pregeneratedResponse?: string;
sidePanelContext?: string;
};

export default AddCommentOrAttachmentParams;
45 changes: 40 additions & 5 deletions src/libs/actions/Report/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@
ReportAttributesDerivedValue,
ReportNextStepDeprecated,
ReportUserIsTyping,
SidePanelContext,
Transaction,
TransactionViolations,
VisibleReportActionsDerivedValue,
Expand Down Expand Up @@ -355,6 +356,7 @@
currentUserAccountID: number;
shouldPlaySound?: boolean;
isInSidePanel?: boolean;
sidePanelContext?: SidePanelContext;
pregeneratedResponseParams?: PregeneratedResponseParams;
reportActionID?: string;
delegateAccountID: number | undefined;
Expand All @@ -369,6 +371,7 @@
text?: string;
file?: FileObject;
isInSidePanel?: boolean;
sidePanelContext?: SidePanelContext;
pregeneratedResponseParams?: PregeneratedResponseParams;
reportActionID?: string;
delegateAccountID: number | undefined;
Expand All @@ -385,13 +388,14 @@
shouldPlaySound?: boolean;
isInSidePanel?: boolean;
delegateAccountID: number | undefined;
sidePanelContext?: SidePanelContext;
};

const addNewMessageWithText = new Set<string>([WRITE_COMMANDS.ADD_COMMENT, WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT]);
// map of reportID to all reportActions for that report
const allReportActions: OnyxCollection<ReportActions> = {};

Onyx.connect({

Check warning on line 398 in src/libs/actions/Report/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
callback: (actions, key) => {
if (!key || !actions) {
Expand All @@ -403,7 +407,7 @@
});

let allReports: OnyxCollection<Report>;
Onyx.connect({

Check warning on line 410 in src/libs/actions/Report/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -412,7 +416,7 @@
});

let allPersonalDetails: OnyxEntry<PersonalDetailsList> = {};
Onyx.connect({

Check warning on line 419 in src/libs/actions/Report/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
allPersonalDetails = value ?? {};
Expand All @@ -427,7 +431,7 @@
});

let onboarding: OnyxEntry<Onboarding>;
Onyx.connect({

Check warning on line 434 in src/libs/actions/Report/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.NVP_ONBOARDING,
callback: (val) => {
if (Array.isArray(val)) {
Expand Down Expand Up @@ -682,6 +686,7 @@
text = '',
file,
isInSidePanel = false,
sidePanelContext,
pregeneratedResponseParams,
reportActionID,
delegateAccountID,
Expand Down Expand Up @@ -797,21 +802,26 @@
idempotencyKey: Str.guid(),
};

if (reportIDDeeplinkedFromOldDot === reportID && isConciergeChatReport(report)) {
const isConciergeChat = isConciergeChatReport(report);
if (reportIDDeeplinkedFromOldDot === reportID && isConciergeChat) {
parameters.isOldDotConciergeChat = true;
}

if (file) {
parameters.attachmentID = attachmentID;
}

if (isInSidePanel && (isConciergeChatReport(report) || isAdminRoom(report))) {
if (isInSidePanel && (isConciergeChat || isAdminRoom(report))) {
const pageHTML = capturePageHTML();
if (pageHTML) {
parameters.pageHTML = pageHTML;
}
}

if (isInSidePanel && isConciergeChat && sidePanelContext && commandName === WRITE_COMMANDS.ADD_COMMENT) {
parameters.sidePanelContext = JSON.stringify(sidePanelContext);
}

// Add pregenerated params
if (pregeneratedResponseParams) {
parameters.optimisticConciergeReportActionID = pregeneratedResponseParams.optimisticConciergeReportActionID;
Expand Down Expand Up @@ -938,6 +948,7 @@
shouldPlaySound = false,
isInSidePanel = false,
delegateAccountID,
sidePanelContext,
}: AddAttachmentWithCommentParams) {
if (!report?.reportID) {
return;
Expand All @@ -952,7 +963,7 @@

// Single attachment
if (!Array.isArray(attachments)) {
addActions({report, notifyReportID, ancestors, timezoneParam: timezone, currentUserAccountID, text, file: attachments, isInSidePanel, delegateAccountID});
addActions({report, notifyReportID, ancestors, timezoneParam: timezone, currentUserAccountID, text, file: attachments, isInSidePanel, delegateAccountID, sidePanelContext});
handlePlaySound();
return;
}
Expand All @@ -962,7 +973,18 @@

// Remaining: attachment-only actions (no text duplication)
for (let i = 1; i < attachments?.length; i += 1) {
addActions({report, notifyReportID, ancestors, timezoneParam: timezone, currentUserAccountID, text: '', file: attachments?.at(i), isInSidePanel, delegateAccountID});
addActions({
report,
notifyReportID,
ancestors,
timezoneParam: timezone,
currentUserAccountID,
text: '',
file: attachments?.at(i),
isInSidePanel,
delegateAccountID,
sidePanelContext,
});
}

// Play sound once
Expand All @@ -979,14 +1001,27 @@
currentUserAccountID,
shouldPlaySound,
isInSidePanel,
sidePanelContext,
pregeneratedResponseParams,
reportActionID,
delegateAccountID,
}: AddCommentParams) {
if (shouldPlaySound) {
playSound(SOUNDS.DONE);
}
addActions({report, notifyReportID, ancestors, timezoneParam, currentUserAccountID, text, isInSidePanel, pregeneratedResponseParams, reportActionID, delegateAccountID});
addActions({
report,
notifyReportID,
ancestors,
timezoneParam,
currentUserAccountID,
text,
isInSidePanel,
pregeneratedResponseParams,
reportActionID,
delegateAccountID,
sidePanelContext,
});
}

function reportActionsExist(reportID: string): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type * as OnyxTypes from '@src/types/onyx';
import {useComposerMeta} from './ComposerContext';
import useSidePanelContext from './useSidePanelContext';

function useComposerSubmit(reportID: string): (comment: string) => void {
const {isOffline} = useNetwork();
Expand All @@ -34,6 +35,7 @@ function useComposerSubmit(reportID: string): (comment: string) => void {
const personalDetails = usePersonalDetails();
const {availableLoginsList} = useShortMentionsList();
const isInSidePanel = useIsInSidePanel();
const sidePanelContext = useSidePanelContext(reportID);
const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE);
const delegateAccountID = useDelegateAccountID();

Expand Down Expand Up @@ -75,6 +77,7 @@ function useComposerSubmit(reportID: string): (comment: string) => void {
shouldPlaySound: true,
isInSidePanel,
delegateAccountID,
sidePanelContext,
});
attachmentFileRef.current = null;
return;
Expand Down Expand Up @@ -145,6 +148,7 @@ function useComposerSubmit(reportID: string): (comment: string) => void {
currentUserAccountID: currentUserPersonalDetails.accountID,
shouldPlaySound: true,
isInSidePanel,
sidePanelContext,
reportActionID: optimisticReportActionID,
delegateAccountID,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {useMemo} from 'react';
import {useSearchStateContext} from '@components/Search/SearchContext';
import {useCurrentReportIDState} from '@hooks/useCurrentReportID';
import useIsInSidePanel from '@hooks/useIsInSidePanel';
import useOnyx from '@hooks/useOnyx';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type * as OnyxTypes from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';

function useSidePanelContext(reportID: string): OnyxTypes.SidePanelContext | undefined {
const isInSidePanel = useIsInSidePanel();
const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID);
const {currentReportID, currentRHPReportID} = useCurrentReportIDState();
const {currentSearchQueryJSON, selectedTransactionIDs, selectedTransactions, selectedReports} = useSearchStateContext();

return useMemo(() => {
if (conciergeReportID !== reportID || !isInSidePanel) {
return undefined;
}

const contextReportID = currentRHPReportID ?? currentReportID ?? undefined;

// selectedTransactions (map) is populated from the Search list; selectedTransactionIDs (array)
// is populated from the report table view. The two are mutually exclusive.
const txIDsFromMap = !isEmptyObject(selectedTransactions)
? Object.entries(selectedTransactions)
.filter(([, info]) => info.isSelected && !!info.transaction)
.map(([id]) => id)
: [];
const allTransactionIDs = txIDsFromMap.length > 0 ? txIDsFromMap : selectedTransactionIDs;
const selectedTransactionIDsForContext = allTransactionIDs.length > 0 ? allTransactionIDs.join(',') : undefined;

const selectedReportIDsForContext =
selectedReports.length > 0
? selectedReports
.map((r) => r.reportID)
.filter((id): id is string => !!id)
.join(',') || undefined
: undefined;

// This condition is reached when we are either in the global Reports => Reports page, or within a single expense report having multiple transactions.
// If we have selectedReportIDs, that means we're in the Reports page, otherwise we're in the expense report RHP.
if (currentSearchQueryJSON?.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT) {
return selectedReportIDsForContext ? {selectedReportIDs: selectedReportIDsForContext} : {reportID: contextReportID, selectedTransactionIDs: selectedTransactionIDsForContext};
Comment thread
youssef-lr marked this conversation as resolved.
}

if (!contextReportID && !selectedTransactionIDsForContext && !selectedReportIDsForContext) {
return undefined;
}

return {reportID: contextReportID, selectedTransactionIDs: selectedTransactionIDsForContext, selectedReportIDs: selectedReportIDsForContext};
}, [conciergeReportID, reportID, isInSidePanel, currentSearchQueryJSON?.type, currentRHPReportID, currentReportID, selectedTransactionIDs, selectedTransactions, selectedReports]);
}

export default useSidePanelContext;
7 changes: 7 additions & 0 deletions src/types/onyx/SidePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,11 @@ type SidePanel = {
openNarrowScreen: boolean;
};

/**
* Describes the context of what the user was viewing when they sent a message from the Side Panel.
* Sent to the backend so Concierge can tailor its response to the user's current context.
*/
type SidePanelContext = {reportID?: string; selectedTransactionIDs?: string; selectedReportIDs?: string};

export default SidePanel;
export type {SidePanelContext};
2 changes: 2 additions & 0 deletions src/types/onyx/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ import type Session from './Session';
import type ShareBankAccount from './ShareBankAccount';
import type ShareTempFile from './ShareTempFile';
import type SidePanel from './SidePanel';
import type {SidePanelContext} from './SidePanel';
import type StripeCustomerID from './StripeCustomerID';
import type SupportalPermissionDenied from './SupportalPermissionDenied';
import type Task from './Task';
Expand Down Expand Up @@ -362,6 +363,7 @@ export type {
DismissedProductTraining,
TravelProvisioning,
SidePanel,
SidePanelContext,
LastPaymentMethodType,
ReportAttributesDerivedValue,
LastSearchParams,
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/SuggestionMentionTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ describe('SuggestionMention', () => {

mockUsePersonalDetails.mockImplementation(() => mockPersonalDetails);
mockUseArrowKeyFocusManager.mockReturnValue([0, mockSetHighlightedMentionIndex]);
mockUseCurrentReportIDState.mockReturnValue({currentReportID: ''});
mockUseCurrentReportIDState.mockReturnValue({currentReportID: '', currentRHPReportID: ''});
mockUseCurrentUserPersonalDetails.mockReturnValue({accountID: 1, login: 'current@gmail.com'});
mockUseDebounce.mockImplementation((callback) => {
const callbackRef = React.useRef(callback);
Expand Down
Loading
Loading