From bcf112254c6977e70f71958801729c4dbe70af0d Mon Sep 17 00:00:00 2001 From: mhawryluk Date: Fri, 24 Apr 2026 16:57:42 +0200 Subject: [PATCH 01/16] "Ask Concierge" option in the search router --- src/CONST/index.ts | 1 + .../Search/SearchAutocompleteList.tsx | 12 ++-- .../SearchPageInputNarrow.tsx | 4 +- .../SearchPageHeader/SearchPageInputWide.tsx | 4 +- .../SearchPageHeader/useSearchPageInput.tsx | 22 ++++--- .../Search/SearchRouter/SearchRouter.tsx | 65 +++++++++++++++---- src/languages/en.ts | 1 + 7 files changed, 76 insertions(+), 33 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 8389b5fd7c8b..1db309d09d4f 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -7974,6 +7974,7 @@ const CONST = { AUTOCOMPLETE_SUGGESTION: 'autocompleteSuggestion', SEARCH: 'searchItem', FIND_ITEM: 'findItem', + ASK_CONCIERGE: 'askConcierge', }, SEARCH_USER_FRIENDLY_KEYS: { TYPE: 'type', diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index 23bd7dfb9bf4..384b43feadad 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -55,8 +55,8 @@ type SearchAutocompleteListProps = { /** Callback to trigger search action * */ handleSearch: (value: string) => void; - /** An optional item to always display on the top of the router list */ - searchQueryItem?: SearchQueryItem; + /** Optional items to always display at the top of the router list */ + searchQueryItems?: SearchQueryItem[]; /** Any extra sections that should be displayed in the router list. */ getAdditionalSections?: GetAdditionalSectionsCallback; @@ -131,7 +131,7 @@ function SearchRouterItem(props: UserListItemProps | Searc function SearchAutocompleteList({ autocompleteQueryValue, handleSearch, - searchQueryItem, + searchQueryItems, getAdditionalSections, onListItemPress, shouldSubscribeToArrowKeyEvents = true, @@ -406,8 +406,8 @@ function SearchAutocompleteList({ nextSuggestionsCount += section.data.filter((item) => item.keyForList !== CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.FIND_ITEM).length; }; - if (searchQueryItem) { - pushSection({data: [searchQueryItem as AutocompleteListItem], sectionIndex: sectionIndex++}); + if (searchQueryItems && searchQueryItems.length > 0) { + pushSection({data: searchQueryItems as AutocompleteListItem[], sectionIndex: sectionIndex++}); } const additionalSections = getAdditionalSections?.(searchOptions, sectionIndex); @@ -497,7 +497,7 @@ function SearchAutocompleteList({ recentReportsOptions, recentSearchesData, searchOptions, - searchQueryItem, + searchQueryItems, styles, translate, isLoadingOptions, diff --git a/src/components/Search/SearchPageHeader/SearchPageInputNarrow.tsx b/src/components/Search/SearchPageHeader/SearchPageInputNarrow.tsx index a18c79424dde..a141048624e1 100644 --- a/src/components/Search/SearchPageHeader/SearchPageInputNarrow.tsx +++ b/src/components/Search/SearchPageHeader/SearchPageInputNarrow.tsx @@ -25,7 +25,7 @@ function SearchPageInputNarrow({queryJSON, searchRouterListVisible, hideSearchRo const { autocompleteSubstitutions, autocompleteQueryValue, - searchQueryItem, + searchQueryItems, selection, textInputRef, textInputValue, @@ -80,7 +80,7 @@ function SearchPageInputNarrow({queryJSON, searchRouterListVisible, hideSearchRo (null); - const expensifyIcons = useMemoizedLazyExpensifyIcons(['MagnifyingGlass']); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['MagnifyingGlass', 'ConciergeAvatar']); + const {openSidePanel} = useSidePanelActions(); // The actual input text that the user sees const [textInputValue, , setTextInputValue] = useDebouncedState('', 500); @@ -197,15 +201,25 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla ], ); - const searchQueryItem = textInputValue - ? { - text: textInputValue, - singleIcon: expensifyIcons.MagnifyingGlass, - searchQuery: textInputValue, - itemStyle: styles.activeComponentBG, - keyForList: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.FIND_ITEM, - searchItemType: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.SEARCH, - } + const searchQueryItems = textInputValue + ? [ + { + text: textInputValue, + singleIcon: expensifyIcons.MagnifyingGlass, + searchQuery: textInputValue, + itemStyle: styles.activeComponentBG, + keyForList: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.FIND_ITEM, + searchItemType: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.SEARCH, + }, + { + text: translate('search.askConcierge', textInputValue), + singleIcon: expensifyIcons.ConciergeAvatar, + searchQuery: textInputValue, + itemStyle: styles.activeComponentBG, + keyForList: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.ASK_CONCIERGE, + searchItemType: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.ASK_CONCIERGE, + }, + ] : undefined; const shouldScrollRef = useRef(false); @@ -307,6 +321,25 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla setAutocompleteSubstitutions, }); setFocusAndScrollToRight(); + } else if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.ASK_CONCIERGE) { + onRouterClose(); + setTextInputValue(''); + setAutocompleteQueryValue(''); + if (!conciergeReportID || !conciergeReport || !item.searchQuery) { + return; + } + openSidePanel(); + + addComment({ + report: conciergeReport, + notifyReportID: conciergeReportID, + ancestors: [], + text: item.searchQuery, + timezoneParam: currentUserPersonalDetails.timezone ?? CONST.DEFAULT_TIME_ZONE, + currentUserAccountID, + shouldPlaySound: true, + isInSidePanel: true, + }); } else { submitSearch(item.searchQuery, item.keyForList !== CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.FIND_ITEM); } @@ -334,13 +367,19 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla betas, contextualPoliciesMap, contextualReportsMap, + conciergeReportID, + conciergeReport, + setTextInputValue, + setAutocompleteQueryValue, + currentUserPersonalDetails.timezone, + openSidePanel, ], ); useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => { onRouterClose(); }); - const updateAndScrollToFocusedIndex = useCallback(() => listRef.current?.updateAndScrollToFocusedIndex(1, true), []); + const updateAndScrollToFocusedIndex = useCallback(() => listRef.current?.updateAndScrollToFocusedIndex(searchQueryItems?.length ?? 1, true), [searchQueryItems?.length]); const modalWidth = shouldUseNarrowLayout ? styles.w100 : {width: variables.searchRouterPopoverWidth}; @@ -386,7 +425,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla `Ask Concierge “${message}”`, searchPlaceholder: 'Search for something...', suggestions: 'Suggestions', suggestionsAvailable: ({count}: {count: number}, query = '') => ({ From 37d9f120f382dd85baeb6416eb3283d8a696ff06 Mon Sep 17 00:00:00 2001 From: mhawryluk Date: Fri, 24 Apr 2026 17:53:09 +0200 Subject: [PATCH 02/16] Fix message hidden behind "show history" when sending while offline --- .../Search/SearchRouter/SearchRouter.tsx | 7 ++++++- .../SidePanel/SidePanelContextProvider.tsx | 15 ++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 14135d1011fc..264f7fe6a4ac 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -24,6 +24,7 @@ import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useRootNavigationState from '@hooks/useRootNavigationState'; import useSidePanelActions from '@hooks/useSidePanelActions'; +import useSidePanelState from '@hooks/useSidePanelState'; import useThemeStyles from '@hooks/useThemeStyles'; import {scrollToRight} from '@libs/InputUtils'; import backHistory from '@libs/Navigation/helpers/backHistory'; @@ -76,6 +77,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla const listRef = useRef(null); const expensifyIcons = useMemoizedLazyExpensifyIcons(['MagnifyingGlass', 'ConciergeAvatar']); const {openSidePanel} = useSidePanelActions(); + const {shouldHideSidePanel: isSidePanelOpen} = useSidePanelState(); // The actual input text that the user sees const [textInputValue, , setTextInputValue] = useDebouncedState('', 500); @@ -328,7 +330,9 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla if (!conciergeReportID || !conciergeReport || !item.searchQuery) { return; } - openSidePanel(); + if (isSidePanelOpen) { + openSidePanel(); + } addComment({ report: conciergeReport, @@ -373,6 +377,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla setAutocompleteQueryValue, currentUserPersonalDetails.timezone, openSidePanel, + isSidePanelOpen, ], ); diff --git a/src/components/SidePanel/SidePanelContextProvider.tsx b/src/components/SidePanel/SidePanelContextProvider.tsx index 0d76f918f3f7..3f29e187bc03 100644 --- a/src/components/SidePanel/SidePanelContextProvider.tsx +++ b/src/components/SidePanel/SidePanelContextProvider.tsx @@ -89,14 +89,6 @@ function SidePanelContextProvider({children}: PropsWithChildren) { const reportID = (isRHPAdminsRoom || isRHPHomePage) && isUserAdmin && isPolicyActive && adminsChatReportID ? adminsChatReportID : conciergeReportID; const [sessionStartTime, setSessionStartTime] = useState(null); - const [prevShouldHideSidePanel, setPrevShouldHideSidePanel] = useState(shouldHideSidePanel); - - if (prevShouldHideSidePanel !== shouldHideSidePanel) { - setPrevShouldHideSidePanel(shouldHideSidePanel); - if (!shouldHideSidePanel) { - setSessionStartTime(DateUtils.getDBTime()); - } - } useEffect(() => { sidePanelWidthRef.current = sidePanelWidth; @@ -135,6 +127,11 @@ function SidePanelContextProvider({children}: PropsWithChildren) { focusComposerWithDelay(ReportActionComposeFocusManager.composerRef.current, CONST.SIDE_PANEL_ANIMATED_TRANSITION + CONST.COMPOSER_FOCUS_DELAY)(true); }; + const openSidePanel = () => { + setSessionStartTime(DateUtils.getDBTime()); + SidePanelActions.openSidePanel(!isExtraLargeScreenWidth); + }; + // Because of the React Compiler we don't need to memoize it manually // eslint-disable-next-line react/jsx-no-constructed-context-values const stateValue = { @@ -153,7 +150,7 @@ function SidePanelContextProvider({children}: PropsWithChildren) { // Because of the React Compiler we don't need to memoize it manually // eslint-disable-next-line react/jsx-no-constructed-context-values const actionsValue = { - openSidePanel: () => SidePanelActions.openSidePanel(!isExtraLargeScreenWidth), + openSidePanel, closeSidePanel, }; From 48c07d24cb0ed6eeb8373e85935f377620a18d71 Mon Sep 17 00:00:00 2001 From: mhawryluk Date: Mon, 27 Apr 2026 13:52:42 +0200 Subject: [PATCH 03/16] Add translations --- src/languages/de.ts | 1 + src/languages/es.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + 9 files changed, 9 insertions(+) diff --git a/src/languages/de.ts b/src/languages/de.ts index 02be2176c859..c68992b35533 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7779,6 +7779,7 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc recentSearches: 'Letzte Suchen', recentChats: 'Neueste Chats', searchIn: 'Suchen in', + askConcierge: (message: string) => `Frage Concierge „${message}“`, searchPlaceholder: 'Nach etwas suchen...', suggestions: 'Vorschläge', suggestionsAvailable: ( diff --git a/src/languages/es.ts b/src/languages/es.ts index 2dfcb86d7491..dbc4a372fd0d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7634,6 +7634,7 @@ ${amount} para ${merchant} - ${date}`, recentSearches: 'Búsquedas recientes', recentChats: 'Chats recientes', searchIn: 'Buscar en', + askConcierge: (message: string) => `Preguntar a Concierge “${message}”`, searchPlaceholder: 'Busca algo...', suggestions: 'Sugerencias', suggestionsAvailable: ({count}: {count: number}, query = '') => ({ diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 0f17d9c917fb..a889246cff05 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7801,6 +7801,7 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e recentSearches: 'Recherches récentes', recentChats: 'Discussions récentes', searchIn: 'Rechercher dans', + askConcierge: (message: string) => `Demander à Concierge « ${message} »`, searchPlaceholder: 'Rechercher quelque chose...', suggestions: 'Suggestions', suggestionsAvailable: ( diff --git a/src/languages/it.ts b/src/languages/it.ts index 2e5e210f509b..74087ac79b80 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7768,6 +7768,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, recentSearches: 'Ricerche recenti', recentChats: 'Chat recenti', searchIn: 'Cerca in', + askConcierge: (message: string) => `Chiedi a Concierge “${message}”`, searchPlaceholder: 'Cerca qualcosa...', suggestions: 'Suggerimenti', suggestionsAvailable: ( diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 266d0c950f49..090269fdae0d 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7671,6 +7671,7 @@ ${reportName} recentSearches: '最近の検索', recentChats: '最近のチャット', searchIn: '検索対象', + askConcierge: (message: string) => `Concierge に「${message}」と聞く`, searchPlaceholder: '何かを検索...', suggestions: '提案', suggestionsAvailable: ( diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 3ffdb30e6426..e01e21b83350 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7731,6 +7731,7 @@ Voeg meer bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, recentSearches: 'Recente zoekopdrachten', recentChats: 'Recente chats', searchIn: 'Zoeken in', + askConcierge: (message: string) => `Vraag Concierge: “${message}”`, searchPlaceholder: 'Zoek iets...', suggestions: 'Suggesties', suggestionsAvailable: ( diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 06cf4d88873c..3d02ea9e1a1c 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7733,6 +7733,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, recentSearches: 'Ostatnie wyszukiwania', recentChats: 'Ostatnie czaty', searchIn: 'Szukaj w', + askConcierge: (message: string) => `Zapytaj Concierge: „${message}”`, searchPlaceholder: 'Wyszukaj coś...', suggestions: 'Sugestie', suggestionsAvailable: ( diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 0acae2f2d9f7..1ac8ebcd4aee 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7719,6 +7719,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, recentSearches: 'Pesquisas recentes', recentChats: 'Chats recentes', searchIn: 'Pesquisar em', + askConcierge: (message: string) => `Perguntar ao Concierge “${message}”`, searchPlaceholder: 'Pesquisar algo...', suggestions: 'Sugestões', suggestionsAvailable: ( diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 308749e97e83..83143a3d9589 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7535,6 +7535,7 @@ ${reportName} recentSearches: '最近搜索', recentChats: '最近聊天', searchIn: '搜索范围', + askConcierge: (message: string) => `询问 Concierge“${message}”`, searchPlaceholder: '搜索内容...', suggestions: '建议', suggestionsAvailable: ( From 70ee607779d8b4d23081224037f31fc7063a8a44 Mon Sep 17 00:00:00 2001 From: mhawryluk Date: Mon, 27 Apr 2026 13:57:23 +0200 Subject: [PATCH 04/16] Fix Spanish translation --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index dbc4a372fd0d..be926e37b8da 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7634,7 +7634,7 @@ ${amount} para ${merchant} - ${date}`, recentSearches: 'Búsquedas recientes', recentChats: 'Chats recientes', searchIn: 'Buscar en', - askConcierge: (message: string) => `Preguntar a Concierge “${message}”`, + askConcierge: (message: string) => `Pregunta a Concierge “${message}”`, searchPlaceholder: 'Busca algo...', suggestions: 'Sugerencias', suggestionsAvailable: ({count}: {count: number}, query = '') => ({ From a26752400f7a98d8529556f9339b3b418f8e8e30 Mon Sep 17 00:00:00 2001 From: mhawryluk Date: Mon, 27 Apr 2026 17:44:28 +0200 Subject: [PATCH 05/16] Navigate to concierge chat on native --- .../Search/SearchRouter/SearchRouter.tsx | 33 ++++--------------- .../Search/SearchRouter/useAskConcierge.tsx | 32 ++++++++++++++++++ src/hooks/useOpenConcierge/index.native.tsx | 19 +++++++++++ src/hooks/useOpenConcierge/index.tsx | 16 +++++++++ src/pages/settings/HelpPage/HelpPage.tsx | 12 +++---- 5 files changed, 77 insertions(+), 35 deletions(-) create mode 100644 src/components/Search/SearchRouter/useAskConcierge.tsx create mode 100644 src/hooks/useOpenConcierge/index.native.tsx create mode 100644 src/hooks/useOpenConcierge/index.tsx diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 264f7fe6a4ac..d946b8b1a7bf 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -23,8 +23,6 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useRootNavigationState from '@hooks/useRootNavigationState'; -import useSidePanelActions from '@hooks/useSidePanelActions'; -import useSidePanelState from '@hooks/useSidePanelState'; import useThemeStyles from '@hooks/useThemeStyles'; import {scrollToRight} from '@libs/InputUtils'; import backHistory from '@libs/Navigation/helpers/backHistory'; @@ -39,7 +37,7 @@ import {getQueryWithUpdatedValues, sanitizeSearchValue} from '@libs/SearchQueryU import StringUtils from '@libs/StringUtils'; import Navigation from '@navigation/Navigation'; import variables from '@styles/variables'; -import {addComment, navigateToAndOpenReport, searchInServer} from '@userActions/Report'; +import {navigateToAndOpenReport, searchInServer} from '@userActions/Report'; import {setSearchContext} from '@userActions/Search'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -50,6 +48,7 @@ import {getQueryWithSubstitutions} from './getQueryWithSubstitutions'; import {getUpdatedSubstitutionsMap} from './getUpdatedSubstitutionsMap'; import {getContextualReportData, getContextualSearchAutocompleteKey, getContextualSearchQuery} from './SearchRouterUtils'; import updateAutocompleteSubstitutionsForSelection from './updateAutocompleteSubstitutionsForSelection'; +import useAskConcierge from './useAskConcierge'; const privateIsArchivedSelector = (nvp: {private_isArchived?: string} | undefined): boolean | undefined => !!nvp?.private_isArchived; @@ -70,14 +69,11 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const [betas] = useOnyx(ONYXKEYS.BETAS); const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); - const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); - const [conciergeReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${conciergeReportID}`); const personalDetails = usePersonalDetails(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const listRef = useRef(null); const expensifyIcons = useMemoizedLazyExpensifyIcons(['MagnifyingGlass', 'ConciergeAvatar']); - const {openSidePanel} = useSidePanelActions(); - const {shouldHideSidePanel: isSidePanelOpen} = useSidePanelState(); + const askConcierge = useAskConcierge(); // The actual input text that the user sees const [textInputValue, , setTextInputValue] = useDebouncedState('', 500); @@ -327,23 +323,10 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla onRouterClose(); setTextInputValue(''); setAutocompleteQueryValue(''); - if (!conciergeReportID || !conciergeReport || !item.searchQuery) { + if (!item.searchQuery) { return; } - if (isSidePanelOpen) { - openSidePanel(); - } - - addComment({ - report: conciergeReport, - notifyReportID: conciergeReportID, - ancestors: [], - text: item.searchQuery, - timezoneParam: currentUserPersonalDetails.timezone ?? CONST.DEFAULT_TIME_ZONE, - currentUserAccountID, - shouldPlaySound: true, - isInSidePanel: true, - }); + askConcierge(item.searchQuery); } else { submitSearch(item.searchQuery, item.keyForList !== CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.FIND_ITEM); } @@ -371,13 +354,9 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla betas, contextualPoliciesMap, contextualReportsMap, - conciergeReportID, - conciergeReport, setTextInputValue, setAutocompleteQueryValue, - currentUserPersonalDetails.timezone, - openSidePanel, - isSidePanelOpen, + askConcierge, ], ); diff --git a/src/components/Search/SearchRouter/useAskConcierge.tsx b/src/components/Search/SearchRouter/useAskConcierge.tsx new file mode 100644 index 000000000000..0808317ca10a --- /dev/null +++ b/src/components/Search/SearchRouter/useAskConcierge.tsx @@ -0,0 +1,32 @@ +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useOnyx from '@hooks/useOnyx'; +import useOpenConcierge from '@hooks/useOpenConcierge'; +import {addComment} from '@userActions/Report'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +function useAskConcierge() { + const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); + const [conciergeReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${conciergeReportID}`); + const {timezone, accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); + const openConcierge = useOpenConcierge(); + + return (searchQuery: string) => { + openConcierge(); + if (!conciergeReport || !conciergeReportID) { + return; + } + addComment({ + report: conciergeReport, + notifyReportID: conciergeReportID, + ancestors: [], + text: searchQuery, + timezoneParam: timezone ?? CONST.DEFAULT_TIME_ZONE, + currentUserAccountID, + shouldPlaySound: true, + isInSidePanel: true, + }); + }; +} + +export default useAskConcierge; diff --git a/src/hooks/useOpenConcierge/index.native.tsx b/src/hooks/useOpenConcierge/index.native.tsx new file mode 100644 index 000000000000..186d4a6c7e60 --- /dev/null +++ b/src/hooks/useOpenConcierge/index.native.tsx @@ -0,0 +1,19 @@ +import {hasSeenTourSelector} from '@selectors/Onboarding'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useOnyx from '@hooks/useOnyx'; +import {navigateToConciergeChat} from '@userActions/Report'; +import ONYXKEYS from '@src/ONYXKEYS'; + +function useOpenConcierge() { + const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); + const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); + const [betas] = useOnyx(ONYXKEYS.BETAS); + const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); + const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); + + return () => { + navigateToConciergeChat(conciergeReportID, introSelected, currentUserAccountID, isSelfTourViewed, betas); + }; +} + +export default useOpenConcierge; diff --git a/src/hooks/useOpenConcierge/index.tsx b/src/hooks/useOpenConcierge/index.tsx new file mode 100644 index 000000000000..4181cef5bb68 --- /dev/null +++ b/src/hooks/useOpenConcierge/index.tsx @@ -0,0 +1,16 @@ +import useSidePanelActions from '@hooks/useSidePanelActions'; +import useSidePanelState from '@hooks/useSidePanelState'; + +function useOpenConcierge() { + const {shouldHideSidePanel: isSidePanelOpen} = useSidePanelState(); + const {openSidePanel} = useSidePanelActions(); + + return () => { + if (isSidePanelOpen) { + return; + } + openSidePanel(); + }; +} + +export default useOpenConcierge; diff --git a/src/pages/settings/HelpPage/HelpPage.tsx b/src/pages/settings/HelpPage/HelpPage.tsx index 1a272dd79724..544c54504da1 100644 --- a/src/pages/settings/HelpPage/HelpPage.tsx +++ b/src/pages/settings/HelpPage/HelpPage.tsx @@ -10,13 +10,12 @@ import useIsPaidPolicyAdmin from '@hooks/useIsPaidPolicyAdmin'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import useOpenConcierge from '@hooks/useOpenConcierge'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useSidePanelActions from '@hooks/useSidePanelActions'; import useThemeStyles from '@hooks/useThemeStyles'; import {openHelpPage} from '@libs/actions/Help'; import {openExternalLink} from '@libs/actions/Link'; -import {navigateToAndOpenReportWithAccountIDs, navigateToConciergeChat} from '@libs/actions/Report'; -import getPlatform from '@libs/getPlatform'; +import {navigateToAndOpenReportWithAccountIDs} from '@libs/actions/Report'; import Navigation from '@libs/Navigation/Navigation'; import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; import colors from '@styles/theme/colors'; @@ -24,8 +23,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {hasSeenTourSelector} from '@src/selectors/Onboarding'; -const isWeb = getPlatform() === CONST.PLATFORM.WEB; - function HelpPage() { const icons = useMemoizedLazyExpensifyIcons(['ConciergeAvatar', 'NewWindow', 'Monitor']); const illustrations = useMemoizedLazyIllustrations(['Chalkboard', 'LifeRing', 'TopiaryDollarSign']); @@ -38,12 +35,11 @@ function HelpPage() { const accountManagerDetails = account?.accountManagerAccountID ? personalDetails?.[account.accountManagerAccountID] : null; const partnerManagerDetails = account?.partnerManagerAccountID ? personalDetails?.[account.partnerManagerAccountID] : null; const guideDetails = account?.guideDetails?.email ? getPersonalDetailByEmail(account.guideDetails.email) : null; - const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); const [betas] = useOnyx(ONYXKEYS.BETAS); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); - const {openSidePanel} = useSidePanelActions(); + const openConcierge = useOpenConcierge(); const menuItems = [ { @@ -52,7 +48,7 @@ function HelpPage() { description: translate('initialSettingsPage.helpPage.conciergeChatDescription'), icon: icons.ConciergeAvatar, iconType: CONST.ICON_TYPE_AVATAR, - onPress: () => (isWeb ? openSidePanel() : navigateToConciergeChat(conciergeReportID, introSelected, currentUserAccountID, isSelfTourViewed, betas)), + onPress: openConcierge, shouldShowRightIcon: true, wrapperStyle: [styles.sectionMenuItemTopDescription], sentryLabel: CONST.SENTRY_LABEL.SETTINGS_HELP.CONCIERGE_CHAT, From b087300310b1c6e86636884081728350017dfe74 Mon Sep 17 00:00:00 2001 From: mhawryluk Date: Mon, 27 Apr 2026 18:05:21 +0200 Subject: [PATCH 06/16] Fixes and refactor --- .../Search/SearchRouter/useAskConcierge.tsx | 10 +++++++--- .../index.native.tsx | 7 +++++-- .../index.tsx | 11 +++++++---- src/pages/settings/HelpPage/HelpPage.tsx | 6 +++--- 4 files changed, 22 insertions(+), 12 deletions(-) rename src/hooks/{useOpenConcierge => useOpenConciergeAnywhere}/index.native.tsx (83%) rename src/hooks/{useOpenConcierge => useOpenConciergeAnywhere}/index.tsx (51%) diff --git a/src/components/Search/SearchRouter/useAskConcierge.tsx b/src/components/Search/SearchRouter/useAskConcierge.tsx index 0808317ca10a..746738f8580c 100644 --- a/src/components/Search/SearchRouter/useAskConcierge.tsx +++ b/src/components/Search/SearchRouter/useAskConcierge.tsx @@ -1,18 +1,22 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useOnyx from '@hooks/useOnyx'; -import useOpenConcierge from '@hooks/useOpenConcierge'; +import useOpenConciergeAnywhere from '@hooks/useOpenConciergeAnywhere'; import {addComment} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +/** + * Returns a callback that opens Concierge (side panel on web, chat on native) + * and sends the provided search query as a message. + */ function useAskConcierge() { const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const [conciergeReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${conciergeReportID}`); const {timezone, accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); - const openConcierge = useOpenConcierge(); + const openConciergeAnywhere = useOpenConciergeAnywhere(); return (searchQuery: string) => { - openConcierge(); + openConciergeAnywhere(); if (!conciergeReport || !conciergeReportID) { return; } diff --git a/src/hooks/useOpenConcierge/index.native.tsx b/src/hooks/useOpenConciergeAnywhere/index.native.tsx similarity index 83% rename from src/hooks/useOpenConcierge/index.native.tsx rename to src/hooks/useOpenConciergeAnywhere/index.native.tsx index 186d4a6c7e60..1f3e19e4e89b 100644 --- a/src/hooks/useOpenConcierge/index.native.tsx +++ b/src/hooks/useOpenConciergeAnywhere/index.native.tsx @@ -4,7 +4,10 @@ import useOnyx from '@hooks/useOnyx'; import {navigateToConciergeChat} from '@userActions/Report'; import ONYXKEYS from '@src/ONYXKEYS'; -function useOpenConcierge() { +/** + * Returns a callback that navigates to the Concierge chat on native. + */ +function useOpenConciergeAnywhere() { const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const [betas] = useOnyx(ONYXKEYS.BETAS); @@ -16,4 +19,4 @@ function useOpenConcierge() { }; } -export default useOpenConcierge; +export default useOpenConciergeAnywhere; diff --git a/src/hooks/useOpenConcierge/index.tsx b/src/hooks/useOpenConciergeAnywhere/index.tsx similarity index 51% rename from src/hooks/useOpenConcierge/index.tsx rename to src/hooks/useOpenConciergeAnywhere/index.tsx index 4181cef5bb68..7b8a69b997cf 100644 --- a/src/hooks/useOpenConcierge/index.tsx +++ b/src/hooks/useOpenConciergeAnywhere/index.tsx @@ -1,16 +1,19 @@ import useSidePanelActions from '@hooks/useSidePanelActions'; import useSidePanelState from '@hooks/useSidePanelState'; -function useOpenConcierge() { - const {shouldHideSidePanel: isSidePanelOpen} = useSidePanelState(); +/** + * Returns a callback that opens the Concierge side panel on web. + */ +function useOpenConciergeAnywhere() { + const {shouldHideSidePanel} = useSidePanelState(); const {openSidePanel} = useSidePanelActions(); return () => { - if (isSidePanelOpen) { + if (!shouldHideSidePanel) { return; } openSidePanel(); }; } -export default useOpenConcierge; +export default useOpenConciergeAnywhere; diff --git a/src/pages/settings/HelpPage/HelpPage.tsx b/src/pages/settings/HelpPage/HelpPage.tsx index 544c54504da1..b5d7cdf6e448 100644 --- a/src/pages/settings/HelpPage/HelpPage.tsx +++ b/src/pages/settings/HelpPage/HelpPage.tsx @@ -10,7 +10,7 @@ import useIsPaidPolicyAdmin from '@hooks/useIsPaidPolicyAdmin'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import useOpenConcierge from '@hooks/useOpenConcierge'; +import useOpenConciergeAnywhere from '@hooks/useOpenConciergeAnywhere'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import {openHelpPage} from '@libs/actions/Help'; @@ -39,7 +39,7 @@ function HelpPage() { const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); const [betas] = useOnyx(ONYXKEYS.BETAS); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); - const openConcierge = useOpenConcierge(); + const openConciergeAnywhere = useOpenConciergeAnywhere(); const menuItems = [ { @@ -48,7 +48,7 @@ function HelpPage() { description: translate('initialSettingsPage.helpPage.conciergeChatDescription'), icon: icons.ConciergeAvatar, iconType: CONST.ICON_TYPE_AVATAR, - onPress: openConcierge, + onPress: openConciergeAnywhere, shouldShowRightIcon: true, wrapperStyle: [styles.sectionMenuItemTopDescription], sentryLabel: CONST.SENTRY_LABEL.SETTINGS_HELP.CONCIERGE_CHAT, From 6b905f843de76d4c8c18e012156e77e48b5153a2 Mon Sep 17 00:00:00 2001 From: mhawryluk Date: Mon, 27 Apr 2026 18:39:00 +0200 Subject: [PATCH 07/16] Fix types --- src/components/Search/SearchRouter/useAskConcierge.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/Search/SearchRouter/useAskConcierge.tsx b/src/components/Search/SearchRouter/useAskConcierge.tsx index 746738f8580c..78c12ee98622 100644 --- a/src/components/Search/SearchRouter/useAskConcierge.tsx +++ b/src/components/Search/SearchRouter/useAskConcierge.tsx @@ -1,4 +1,5 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useDelegateAccountID from '@hooks/useDelegateAccountID'; import useOnyx from '@hooks/useOnyx'; import useOpenConciergeAnywhere from '@hooks/useOpenConciergeAnywhere'; import {addComment} from '@userActions/Report'; @@ -14,6 +15,7 @@ function useAskConcierge() { const [conciergeReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${conciergeReportID}`); const {timezone, accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const openConciergeAnywhere = useOpenConciergeAnywhere(); + const delegateAccountID = useDelegateAccountID(); return (searchQuery: string) => { openConciergeAnywhere(); @@ -29,6 +31,7 @@ function useAskConcierge() { currentUserAccountID, shouldPlaySound: true, isInSidePanel: true, + delegateAccountID, }); }; } From dad1de09079a2f1e1de3790972b19e725e0d67d2 Mon Sep 17 00:00:00 2001 From: mhawryluk Date: Tue, 28 Apr 2026 17:07:03 +0200 Subject: [PATCH 08/16] Fix concierge icon rendering in SearchQueryListItem on mobile --- .../Search/SearchList/ListItem/SearchQueryListItem.tsx | 4 +++- src/components/Search/SearchRouter/SearchRouter.tsx | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/Search/SearchList/ListItem/SearchQueryListItem.tsx b/src/components/Search/SearchList/ListItem/SearchQueryListItem.tsx index 642139a7a370..e9e27b845723 100644 --- a/src/components/Search/SearchList/ListItem/SearchQueryListItem.tsx +++ b/src/components/Search/SearchList/ListItem/SearchQueryListItem.tsx @@ -13,6 +13,8 @@ import type IconAsset from '@src/types/utils/IconAsset'; type SearchQueryItem = ListItem & { singleIcon?: IconAsset; + /** Whether to apply the theme fill color to the icon. Set to false for multi-colored icons like avatars. Defaults to true. */ + shouldIconApplyFill?: boolean; searchItemType?: ValueOf; searchQuery?: string; autocompleteID?: string; @@ -56,7 +58,7 @@ function SearchQueryListItem({item, isFocused, showTooltip, onSelectRow, onFocus {!!item.singleIcon && ( diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index d946b8b1a7bf..93a75dd380bb 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -212,6 +212,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla { text: translate('search.askConcierge', textInputValue), singleIcon: expensifyIcons.ConciergeAvatar, + shouldIconApplyFill: false, searchQuery: textInputValue, itemStyle: styles.activeComponentBG, keyForList: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.ASK_CONCIERGE, From 56e5750056b44cf53ec76794d3c269cd65f83415 Mon Sep 17 00:00:00 2001 From: mhawryluk Date: Tue, 28 Apr 2026 17:19:18 +0200 Subject: [PATCH 09/16] Implement review suggestions --- src/components/Search/SearchRouter/SearchRouter.tsx | 6 +++--- src/components/Search/SearchRouter/useAskConcierge.tsx | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 93a75dd380bb..a3bfe4b175a3 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -321,12 +321,12 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla }); setFocusAndScrollToRight(); } else if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.ASK_CONCIERGE) { - onRouterClose(); - setTextInputValue(''); - setAutocompleteQueryValue(''); if (!item.searchQuery) { return; } + onRouterClose(); + setTextInputValue(''); + setAutocompleteQueryValue(''); askConcierge(item.searchQuery); } else { submitSearch(item.searchQuery, item.keyForList !== CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.FIND_ITEM); diff --git a/src/components/Search/SearchRouter/useAskConcierge.tsx b/src/components/Search/SearchRouter/useAskConcierge.tsx index 78c12ee98622..448a56c50c64 100644 --- a/src/components/Search/SearchRouter/useAskConcierge.tsx +++ b/src/components/Search/SearchRouter/useAskConcierge.tsx @@ -2,6 +2,7 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useDelegateAccountID from '@hooks/useDelegateAccountID'; import useOnyx from '@hooks/useOnyx'; import useOpenConciergeAnywhere from '@hooks/useOpenConciergeAnywhere'; +import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {addComment} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -12,7 +13,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; */ function useAskConcierge() { const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); - const [conciergeReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${conciergeReportID}`); + const [conciergeReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(conciergeReportID)}`); const {timezone, accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const openConciergeAnywhere = useOpenConciergeAnywhere(); const delegateAccountID = useDelegateAccountID(); From 5fd520ebe017464e49d7b02a02048235a95f2e46 Mon Sep 17 00:00:00 2001 From: mhawryluk Date: Tue, 28 Apr 2026 17:36:32 +0200 Subject: [PATCH 10/16] Use backHistory in the concierge search item handler too --- src/components/Search/SearchRouter/SearchRouter.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index a3bfe4b175a3..949159dc3100 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -321,13 +321,11 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla }); setFocusAndScrollToRight(); } else if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.ASK_CONCIERGE) { - if (!item.searchQuery) { - return; - } + const {searchQuery} = item; + backHistory(() => { + askConcierge(searchQuery); + }); onRouterClose(); - setTextInputValue(''); - setAutocompleteQueryValue(''); - askConcierge(item.searchQuery); } else { submitSearch(item.searchQuery, item.keyForList !== CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.FIND_ITEM); } @@ -355,8 +353,6 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla betas, contextualPoliciesMap, contextualReportsMap, - setTextInputValue, - setAutocompleteQueryValue, askConcierge, ], ); From cf882e1f7c7606810961c5e4b0e57c81861d7291 Mon Sep 17 00:00:00 2001 From: mhawryluk Date: Tue, 28 Apr 2026 17:50:52 +0200 Subject: [PATCH 11/16] Update useOpenConciergeAnywhere comments --- src/hooks/useOpenConciergeAnywhere/index.native.tsx | 2 +- src/hooks/useOpenConciergeAnywhere/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useOpenConciergeAnywhere/index.native.tsx b/src/hooks/useOpenConciergeAnywhere/index.native.tsx index 1f3e19e4e89b..b31c32f7c3f1 100644 --- a/src/hooks/useOpenConciergeAnywhere/index.native.tsx +++ b/src/hooks/useOpenConciergeAnywhere/index.native.tsx @@ -5,7 +5,7 @@ import {navigateToConciergeChat} from '@userActions/Report'; import ONYXKEYS from '@src/ONYXKEYS'; /** - * Returns a callback that navigates to the Concierge chat on native. + * Returns a callback that navigates to the Concierge chat on native (opens the side panel on web instead). */ function useOpenConciergeAnywhere() { const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); diff --git a/src/hooks/useOpenConciergeAnywhere/index.tsx b/src/hooks/useOpenConciergeAnywhere/index.tsx index 7b8a69b997cf..1c7bca819ef8 100644 --- a/src/hooks/useOpenConciergeAnywhere/index.tsx +++ b/src/hooks/useOpenConciergeAnywhere/index.tsx @@ -2,7 +2,7 @@ import useSidePanelActions from '@hooks/useSidePanelActions'; import useSidePanelState from '@hooks/useSidePanelState'; /** - * Returns a callback that opens the Concierge side panel on web. + * Returns a callback that opens the Concierge side panel on web (opens the Concierge chat on native instead). */ function useOpenConciergeAnywhere() { const {shouldHideSidePanel} = useSidePanelState(); From d1f1aaaeb88d27006ddd1135da478d073c7523c3 Mon Sep 17 00:00:00 2001 From: mhawryluk Date: Wed, 29 Apr 2026 12:28:03 +0200 Subject: [PATCH 12/16] Use useIsInSidePanel in useAskConcierge --- src/components/Search/SearchRouter/useAskConcierge.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/Search/SearchRouter/useAskConcierge.tsx b/src/components/Search/SearchRouter/useAskConcierge.tsx index 448a56c50c64..672d73fb764e 100644 --- a/src/components/Search/SearchRouter/useAskConcierge.tsx +++ b/src/components/Search/SearchRouter/useAskConcierge.tsx @@ -1,5 +1,6 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDelegateAccountID from '@hooks/useDelegateAccountID'; +import useIsInSidePanel from '@hooks/useIsInSidePanel'; import useOnyx from '@hooks/useOnyx'; import useOpenConciergeAnywhere from '@hooks/useOpenConciergeAnywhere'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; @@ -17,6 +18,7 @@ function useAskConcierge() { const {timezone, accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const openConciergeAnywhere = useOpenConciergeAnywhere(); const delegateAccountID = useDelegateAccountID(); + const isInSidePanel = useIsInSidePanel(); return (searchQuery: string) => { openConciergeAnywhere(); @@ -31,7 +33,7 @@ function useAskConcierge() { timezoneParam: timezone ?? CONST.DEFAULT_TIME_ZONE, currentUserAccountID, shouldPlaySound: true, - isInSidePanel: true, + isInSidePanel, delegateAccountID, }); }; From 252830b9136bf0581d8f285edfcde07e937e4851 Mon Sep 17 00:00:00 2001 From: mhawryluk Date: Wed, 29 Apr 2026 12:43:42 +0200 Subject: [PATCH 13/16] Pass isInSidePanel true on web and false on native in useAskConcierge --- src/components/Search/SearchRouter/useAskConcierge.tsx | 4 +--- src/hooks/useOpenConciergeAnywhere/index.native.tsx | 7 +++++-- src/hooks/useOpenConciergeAnywhere/index.tsx | 7 +++++-- src/pages/settings/HelpPage/HelpPage.tsx | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/components/Search/SearchRouter/useAskConcierge.tsx b/src/components/Search/SearchRouter/useAskConcierge.tsx index 672d73fb764e..03ae2eec8131 100644 --- a/src/components/Search/SearchRouter/useAskConcierge.tsx +++ b/src/components/Search/SearchRouter/useAskConcierge.tsx @@ -1,6 +1,5 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDelegateAccountID from '@hooks/useDelegateAccountID'; -import useIsInSidePanel from '@hooks/useIsInSidePanel'; import useOnyx from '@hooks/useOnyx'; import useOpenConciergeAnywhere from '@hooks/useOpenConciergeAnywhere'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; @@ -16,9 +15,8 @@ function useAskConcierge() { const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const [conciergeReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(conciergeReportID)}`); const {timezone, accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); - const openConciergeAnywhere = useOpenConciergeAnywhere(); + const {openConciergeAnywhere, isInSidePanel} = useOpenConciergeAnywhere(); const delegateAccountID = useDelegateAccountID(); - const isInSidePanel = useIsInSidePanel(); return (searchQuery: string) => { openConciergeAnywhere(); diff --git a/src/hooks/useOpenConciergeAnywhere/index.native.tsx b/src/hooks/useOpenConciergeAnywhere/index.native.tsx index b31c32f7c3f1..c1a7b04f541f 100644 --- a/src/hooks/useOpenConciergeAnywhere/index.native.tsx +++ b/src/hooks/useOpenConciergeAnywhere/index.native.tsx @@ -5,7 +5,8 @@ import {navigateToConciergeChat} from '@userActions/Report'; import ONYXKEYS from '@src/ONYXKEYS'; /** - * Returns a callback that navigates to the Concierge chat on native (opens the side panel on web instead). + * Returns a callback that navigates to the Concierge chat on native (opens the side panel on web instead), + * and a flag indicating that the concierge is not opened in the side panel. */ function useOpenConciergeAnywhere() { const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); @@ -14,9 +15,11 @@ function useOpenConciergeAnywhere() { const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); - return () => { + const openConciergeAnywhere = () => { navigateToConciergeChat(conciergeReportID, introSelected, currentUserAccountID, isSelfTourViewed, betas); }; + + return {openConciergeAnywhere, isInSidePanel: false}; } export default useOpenConciergeAnywhere; diff --git a/src/hooks/useOpenConciergeAnywhere/index.tsx b/src/hooks/useOpenConciergeAnywhere/index.tsx index 1c7bca819ef8..12b68c2a583a 100644 --- a/src/hooks/useOpenConciergeAnywhere/index.tsx +++ b/src/hooks/useOpenConciergeAnywhere/index.tsx @@ -2,18 +2,21 @@ import useSidePanelActions from '@hooks/useSidePanelActions'; import useSidePanelState from '@hooks/useSidePanelState'; /** - * Returns a callback that opens the Concierge side panel on web (opens the Concierge chat on native instead). + * Returns a callback that opens the Concierge side panel on web (opens the Concierge chat on native instead), + * and a flag indicating that the concierge is opened in the side panel. */ function useOpenConciergeAnywhere() { const {shouldHideSidePanel} = useSidePanelState(); const {openSidePanel} = useSidePanelActions(); - return () => { + const openConciergeAnywhere = () => { if (!shouldHideSidePanel) { return; } openSidePanel(); }; + + return {openConciergeAnywhere, isInSidePanel: true}; } export default useOpenConciergeAnywhere; diff --git a/src/pages/settings/HelpPage/HelpPage.tsx b/src/pages/settings/HelpPage/HelpPage.tsx index b5d7cdf6e448..bab8e0f743d8 100644 --- a/src/pages/settings/HelpPage/HelpPage.tsx +++ b/src/pages/settings/HelpPage/HelpPage.tsx @@ -39,7 +39,7 @@ function HelpPage() { const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); const [betas] = useOnyx(ONYXKEYS.BETAS); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); - const openConciergeAnywhere = useOpenConciergeAnywhere(); + const {openConciergeAnywhere} = useOpenConciergeAnywhere(); const menuItems = [ { From ae1ca811a1397973fbe3f40e4432b9e6d418e818 Mon Sep 17 00:00:00 2001 From: mhawryluk Date: Wed, 29 Apr 2026 16:37:13 +0200 Subject: [PATCH 14/16] Add fallback setSessionStartTime during render to SidePanelContextProvider --- src/components/SidePanel/SidePanelContextProvider.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/components/SidePanel/SidePanelContextProvider.tsx b/src/components/SidePanel/SidePanelContextProvider.tsx index 3f29e187bc03..87fb63d098a4 100644 --- a/src/components/SidePanel/SidePanelContextProvider.tsx +++ b/src/components/SidePanel/SidePanelContextProvider.tsx @@ -89,6 +89,16 @@ function SidePanelContextProvider({children}: PropsWithChildren) { const reportID = (isRHPAdminsRoom || isRHPHomePage) && isUserAdmin && isPolicyActive && adminsChatReportID ? adminsChatReportID : conciergeReportID; const [sessionStartTime, setSessionStartTime] = useState(null); + const [prevShouldHideSidePanel, setPrevShouldHideSidePanel] = useState(shouldHideSidePanel); + + if (prevShouldHideSidePanel !== shouldHideSidePanel) { + setPrevShouldHideSidePanel(shouldHideSidePanel); + if (shouldHideSidePanel) { + setSessionStartTime(null); + } else if (!sessionStartTime) { + setSessionStartTime(DateUtils.getDBTime()); + } + } useEffect(() => { sidePanelWidthRef.current = sidePanelWidth; From 7d8d521bba0fc42810922be6c4aa5120998788b3 Mon Sep 17 00:00:00 2001 From: mhawryluk Date: Thu, 30 Apr 2026 09:44:56 +0200 Subject: [PATCH 15/16] Send message to admins chat if it's the one in the side panel on web --- .../Search/SearchRouter/useAskConcierge.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/components/Search/SearchRouter/useAskConcierge.tsx b/src/components/Search/SearchRouter/useAskConcierge.tsx index 03ae2eec8131..7e0931d7a8ea 100644 --- a/src/components/Search/SearchRouter/useAskConcierge.tsx +++ b/src/components/Search/SearchRouter/useAskConcierge.tsx @@ -2,30 +2,33 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useDelegateAccountID from '@hooks/useDelegateAccountID'; import useOnyx from '@hooks/useOnyx'; import useOpenConciergeAnywhere from '@hooks/useOpenConciergeAnywhere'; +import useSidePanelReportID from '@hooks/useSidePanelReportID'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {addComment} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; /** - * Returns a callback that opens Concierge (side panel on web, chat on native) + * Returns a callback that opens the side panel (or Concierge chat on native) * and sends the provided search query as a message. */ function useAskConcierge() { + const sidePanelReportID = useSidePanelReportID(); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); - const [conciergeReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(conciergeReportID)}`); - const {timezone, accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const {openConciergeAnywhere, isInSidePanel} = useOpenConciergeAnywhere(); + const targetReportID = (isInSidePanel ? sidePanelReportID : undefined) ?? conciergeReportID; + const [targetReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(targetReportID)}`); + const {timezone, accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const delegateAccountID = useDelegateAccountID(); return (searchQuery: string) => { openConciergeAnywhere(); - if (!conciergeReport || !conciergeReportID) { + if (!targetReport || !targetReportID) { return; } addComment({ - report: conciergeReport, - notifyReportID: conciergeReportID, + report: targetReport, + notifyReportID: targetReportID, ancestors: [], text: searchQuery, timezoneParam: timezone ?? CONST.DEFAULT_TIME_ZONE, From b5911faff5a7d15954edf2d82b33e3ecc4f5f3e3 Mon Sep 17 00:00:00 2001 From: mhawryluk Date: Thu, 30 Apr 2026 10:34:14 +0200 Subject: [PATCH 16/16] Display "concierge thinking" indicator when asking concierge from the search router --- src/ONYXKEYS.ts | 4 ++++ .../Search/SearchRouter/useAskConcierge.tsx | 3 ++- src/libs/actions/Report/index.ts | 10 ++++++++++ src/pages/inbox/AgentZeroStatusContext.tsx | 13 ++++++++++++- 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 3d21a3c0529a..8973b885c564 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -593,6 +593,9 @@ const ONYXKEYS = { /** Company cards custom names */ NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES: 'nvp_expensify_ccCustomNames', + /** Whether to kick off the "Concierge is thinking" indicator when AgentZeroStatusGate mounts */ + CONCIERGE_THINKING_KICKOFF: 'conciergeThinkingKickoff', + /** The user's Concierge reportID */ CONCIERGE_REPORT_ID: 'conciergeReportID', @@ -1498,6 +1501,7 @@ type OnyxValuesMapping = { [ONYXKEYS.LAST_ROUTE]: string; [ONYXKEYS.IS_USING_IMPORTED_STATE]: boolean; [ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES]: Record; + [ONYXKEYS.CONCIERGE_THINKING_KICKOFF]: boolean; [ONYXKEYS.CONCIERGE_REPORT_ID]: string; [ONYXKEYS.SELF_DM_REPORT_ID]: string; [ONYXKEYS.SHARE_UNKNOWN_USER_DETAILS]: Participant; diff --git a/src/components/Search/SearchRouter/useAskConcierge.tsx b/src/components/Search/SearchRouter/useAskConcierge.tsx index 7e0931d7a8ea..e844dac780a2 100644 --- a/src/components/Search/SearchRouter/useAskConcierge.tsx +++ b/src/components/Search/SearchRouter/useAskConcierge.tsx @@ -4,7 +4,7 @@ import useOnyx from '@hooks/useOnyx'; import useOpenConciergeAnywhere from '@hooks/useOpenConciergeAnywhere'; import useSidePanelReportID from '@hooks/useSidePanelReportID'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; -import {addComment} from '@userActions/Report'; +import {addComment, setConciergeThinkingKickoff} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -26,6 +26,7 @@ function useAskConcierge() { if (!targetReport || !targetReportID) { return; } + setConciergeThinkingKickoff(); addComment({ report: targetReport, notifyReportID: targetReportID, diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 1a01a288d3fc..10d20855e4bb 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -7421,6 +7421,14 @@ function setOptimisticTransactionThread(reportID?: string, parentReportID?: stri }); } +function setConciergeThinkingKickoff() { + Onyx.set(ONYXKEYS.CONCIERGE_THINKING_KICKOFF, true); +} + +function clearConciergeThinkingKickoff() { + Onyx.set(ONYXKEYS.CONCIERGE_THINKING_KICKOFF, null); +} + export type {Video, GuidedSetupData, TaskForParameters, IntroSelected, OpenReportActionParams, ParticipantInfo}; export { @@ -7539,4 +7547,6 @@ export { prepareOnyxDataForCleanUpOptimisticParticipants, getGuidedSetupDataForOpenReport, getReportChannelName, + setConciergeThinkingKickoff, + clearConciergeThinkingKickoff, }; diff --git a/src/pages/inbox/AgentZeroStatusContext.tsx b/src/pages/inbox/AgentZeroStatusContext.tsx index 40c507c3cf12..cdcb22fe89f0 100644 --- a/src/pages/inbox/AgentZeroStatusContext.tsx +++ b/src/pages/inbox/AgentZeroStatusContext.tsx @@ -4,7 +4,7 @@ import React, {createContext, useContext, useEffect, useRef, useState} from 'rea import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; -import {getReportChannelName} from '@libs/actions/Report'; +import {clearConciergeThinkingKickoff, getReportChannelName} from '@libs/actions/Report'; import Log from '@libs/Log'; import Pusher from '@libs/Pusher'; import CONST from '@src/CONST'; @@ -102,6 +102,17 @@ function AgentZeroStatusGate({reportID, children}: React.PropsWithChildren<{repo const lastUpdateTimeRef = useRef(0); const {isOffline} = useNetwork(); + // Auto-kickoff "thinking" indicator when opened from search (where kickoffWaitingIndicator isn't accessible) + const [shouldKickoff] = useOnyx(ONYXKEYS.CONCIERGE_THINKING_KICKOFF); + useEffect(() => { + if (!shouldKickoff) { + return; + } + clearConciergeThinkingKickoff(); + // eslint-disable-next-line react-hooks/set-state-in-effect -- one-shot kickoff from search; Onyx flag is cleared immediately so it cannot cascade + setOptimisticStartTime(Date.now()); + }, [shouldKickoff]); + // Tracks the current agentZeroRequestID so the Pusher callback can detect new requests const agentZeroRequestIDRef = useRef('');