Skip to content

Use the room name for the contextual search suggestion - #96448

Open
mukhrr wants to merge 3 commits into
Expensify:mainfrom
mukhrr:fix/96152
Open

Use the room name for the contextual search suggestion#96448
mukhrr wants to merge 3 commits into
Expensify:mainfrom
mukhrr:fix/96152

Conversation

@mukhrr

@mukhrr mukhrr commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

Fixed both bug found here #96152 (comment) and implement the feature original issue mentions

Fixed Issues

$ #96152
PROPOSAL: #96152 (comment)

Tests

  1. Go to any chat
  2. Click Search icon or CMD+K
  • Verify first item in search, i.e, current chat is focused and can be easily entered into seach input by Enter on web

  • Verify that no errors appear in the JS console

Offline tests

QA Steps

Same as tests

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I verified there are no new alerts related to the canBeMissing param for useOnyx
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I used JaimeGPT to get English > Spanish translation. I then posted it in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
android_app.mp4
Android: mWeb Chrome
android_web.mp4
iOS: Native
ios_app.mp4
iOS: mWeb Safari
ios_web.mp4
MacOS: Chrome / Safari
web.mp4

The "Search in <name>" suggestion falls back to building its own option
when the open report is not in the recent reports list, which is capped
at 500. That fallback passed showPersonalDetails: true unconditionally,
so the label showed a participant's display name instead of the room
name for rooms, group chats and policy expense chats.

Enable showPersonalDetails only for 1:1 chats, and pass the already
subscribed report attributes so the fallback resolves the same derived
name the LHN shows instead of the raw reportName, which is empty for
unnamed group chats.
@marufsharifi

Copy link
Copy Markdown
Contributor

@mukhrr, when do we expect this pr to be ready for review? thanks.

@mukhrr

mukhrr commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@marufsharifi hopefully tomorrow

@marufsharifi

Copy link
Copy Markdown
Contributor

@mukhrr, do you need any help here?

@mukhrr

mukhrr commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

@marufsharifi Almost done, testing my code. thanks!

@mukhrr

mukhrr commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

I found a pre-existing bug here:

  1. Rename any workspace to Acme,Inc — comma, no space (sanitizeSearchValue only quotes on space/NBSP).
  2. Open that workspace's expense chat and press cmd+K → input seeds type:expense workspace:Acme,Inc, unquoted.
  3. Press Enter → URL becomes .../search?q=...policyID:Acme,Inc — the literal name sent as a policy ID.
  4. Result: "Nothing to show", no error. Screenshot in the scratchpad.

Expected

Opening search from a chat seeds the chat's scope, and submitting resolves that name to the report/policy ID:

input:  type:expense workspace:Acme,Inc
URL:    /search?q=type:expense … policyID:26BE5C4005E188DB
result: that workspace's expenses

Actual

The name is submitted verbatim as an ID, and the search silently returns nothing:

input:  type:expense workspace:Acme,Inc      ← unquoted
URL:    /search?q=type:expense … policyID:Acme,Inc   ← literal name, not the ID
result: "Nothing to show"                     ← no error

Root cause

getContextualSearchQuery runs the name through sanitizeSearchValue, but getContextualSearchAutocompleteKey builds the substitution key from the raw name. sanitizeSearchValue only quotes on a space or NBSP — so a comma stays unquoted, the parser splits the value on it, the parsed value no longer equals the key, the substitution is skipped, and getUpdatedFilterValue doesn't normalize in:/policyID:. The literal string goes to the server.

The line-break row is the one case our diff made worse: we normalize the query with lineBreaksToSpaces but not the key, so they can't match.

REC-20260722124217.mp4

@twisterdotcom @marufsharifi do you think we can fix this here or in a separate issue?

@mukhrr
mukhrr marked this pull request as ready for review July 22, 2026 08:22
@mukhrr
mukhrr requested review from a team as code owners July 22, 2026 08:22
@melvin-bot
melvin-bot Bot requested review from marufsharifi and trjExpensify and removed request for a team July 22, 2026 08:22
@melvin-bot

melvin-bot Bot commented Jul 22, 2026

Copy link
Copy Markdown

@marufsharifi Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot
melvin-bot Bot removed the request for review from a team July 22, 2026 08:22

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c4e2467654

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/components/Search/SearchRouter/SearchRouter.tsx Outdated
@mukhrr

mukhrr commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

@marufsharifi my only concern is this #96448 (comment) and I think it's not blocker. The rest can be reviewed

@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
src/components/Search/SearchAutocompleteList.tsx 93.26% <100.00%> (-0.46%) ⬇️
...rc/components/Search/SearchRouter/SearchRouter.tsx 58.44% <ø> (ø)
...c/components/Search/getAutocompleteInitialFocus.ts 100.00% <100.00%> (ø)
... and 8 files with indirect coverage changes

@marufsharifi

Copy link
Copy Markdown
Contributor

@twisterdotcom, could you please share your thoughts on this? Do you think it's necessary to address it in this PR, or would it be okay to handle it in a follow-up PR? Thanks!

Comment on lines +170 to +177
const [[initialQuery, initialSubstitutions]] = useState<[string, SubstitutionMap]>(() => {
if (!currentSearchQueryJSON || !isFromSearchPageSearchButton || !searchContext?.shouldShowSearchQuery) {
return [pendingInitialQuery, {}];
if (pendingInitialQuery || !contextualReport) {
return [pendingInitialQuery, {}];
}

// Built once on mount, so a half-loaded name would stay wrong for as long as the router is open.
// Self DMs carry a fake policy ID that never resolves, so waiting on their policy would never seed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: The seed runs only once, on mount — a slow/cold load silently leaves the input empty with no recovery.

This lazy useState initializer runs a single time. Your own comment on L176 acknowledges it: "Built once on mount, so a half-loaded name would stay wrong…". The failure mode:

  • Deep-link straight into a chat (or hard-refresh on a chat), then immediately hit Cmd+K before Onyx finishes hydrating reportAttributes/policy.
  • The initializer hits the return [pendingInitialQuery, {}] fallback (L173 / L180 / L197) and never re-seeds once the data arrives a few ms later.

Result: an empty search box on exactly the flow this PR is meant to improve, recoverable only by closing and reopening the router.

I understand seeding-after-mount would fight the user's typing, so I'm not asking to make it reactive — but please confirm on a fresh reload directly on a chat that the first Cmd+K still seeds, and document the "context data must be loaded at open time" contract in the PR description.

Comment on lines +200 to +202
const contextualQuery = `${StringUtils.lineBreaksToSpaces(getContextualSearchQuery(contextualTarget, contextualPoliciesMap, contextualReportsMap), true)}${AUTOCOMPLETE_TRAILING_SPACE}`;
const autocompleteKey = getContextualSearchAutocompleteKey(contextualTarget, contextualPoliciesMap, contextualReportsMap);
return [contextualQuery, autocompleteKey && contextualTarget.autocompleteID ? {[autocompleteKey]: contextualTarget.autocompleteID} : {}];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low, pre-existing: sanitize mismatch between the query token and the substitution key can break name→ID resolution.

The query sanitizes the name but the substitution key does not:

// L200 → getContextualSearchQuery → `type:chat in:${sanitizeSearchValue(item.searchQuery)}`
// L201 → getContextualSearchAutocompleteKey → `in:${item.searchQuery}`  // raw, unsanitized

For a name containing a space (a 1:1 DM display name like John Doe, or a room with spaces) the query token becomes in:"John Doe" while the substitution map key is in:John Doe. If the key doesn't match the token, submitting searches the literal name instead of resolving to the reportID.

This mismatch already exists in the onSelectRow click path, so the PR doesn't introduce it — but seeding now fires it automatically on every open from a chat, greatly increasing exposure. Please verify with a DM/room whose name has a space that the seeded query still resolves to the correct report on submit; if not, align both helpers in SearchRouterUtils.ts to sanitize identically.

/** Ends the preceding filter, so what the user types next starts a new term instead of extending that filter's value. */
const AUTOCOMPLETE_TRAILING_SPACE = '\u00A0';

/** Shared by the contextual suggestion and the query the input is seeded with, so both scope the search identically. */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Add an explicit return type to this shared helper.

function getContextualSearchTarget(option: OptionData): ContextualSearchTarget {

Its shape must stay compatible with ContextualSearchTarget (used by getContextualSearchQuery / getContextualSearchAutocompleteKey). Annotating the return catches drift at the definition site rather than only at call sites, and documents the contract — which matters more now that this is reused by both the suggestion row and the seed. You'd need to export/import the type from SearchRouterUtils.ts.


if (option.isInvoiceRoom) {
roomType = CONST.SEARCH.DATA_TYPES.INVOICE;
const report = option as SearchOption<Report>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NAB Type assertion (as) — flagging per our "no type casting" guideline.

const report = option as SearchOption<Report>;

Casts bypass the type checker: if option isn't actually a SearchOption<Report> at runtime, report.item?.invoiceReceiver reads are unsound. This was moved verbatim from the old inline block, so it's pre-existing — but since it now lives in a shared helper, consider a proper narrowing (e.g. a type guard on option.item) instead of the assertion.

Comment on lines +147 to +161
const contextualPoliciesMap = (() => {
if (!contextualReport?.policyID || !contextualReportPolicy) {
return {};
}
const policyKey = `${ONYXKEYS.COLLECTION.POLICY}${contextualReport.policyID}`;
return {[policyKey]: contextualReportPolicy};
})();

const contextualReportsMap = (() => {
if (!contextualReportID || !contextualReport) {
return {};
}
const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${contextualReportID}`;
return {[reportKey]: contextualReport};
})();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NAB contextualPoliciesMap / contextualReportsMap recompute every render via IIFE.

React Compiler will memoize these so there's no runtime concern — flagging only because the IIFE-assigned-to-const pattern reads oddly next to the surrounding hook-based code. useMemo would make the memoization intent explicit. Pre-existing (just relocated), so fine to leave.

expect(result.reportID).toBe('1');
});

it('should keep the room name for a chat room unless showPersonalDetails is enabled', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good test for the room-name fix — but the shared scoping helpers have no coverage.

This test correctly pins the createOptionFromReport room-name behavior ✅. However the actual new logic — getContextualSearchTarget, getContextualSearchQuery, and getContextualSearchAutocompleteKey — is untested. These are pure, exportable functions, so a small table-driven test over {chat, room, invoice individual, invoice non-individual, policy expense, self DM} would cheaply lock in the scoping/substitution logic and guard the shared helper against regressions. I'd like to see that added before merge.

}

function getContextualSearchAutocompleteKey(item: SearchQueryItem, policies: OnyxCollection<OnyxTypes.Policy>, reports?: OnyxCollection<OnyxTypes.Report>) {
type ContextualSearchTarget = Pick<SearchQueryItem, 'searchQuery' | 'autocompleteID' | 'roomType' | 'policyID'>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice use of Pick here.

Pick<SearchQueryItem, 'searchQuery' | 'autocompleteID' | 'roomType' | 'policyID'> is the right, cast-free way to narrow the shared parameter type so both helpers accept the lightweight target object without dragging in the full SearchQueryItem. Consider exporting it so getContextualSearchTarget in SearchRouter.tsx can annotate its return with the same type (see the return-type comment on that function).

@twisterdotcom

Copy link
Copy Markdown
Contributor

Let's fix the workspace name/ID issuse separately, but it does need fixing.

@mukhrr

mukhrr commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Let's fix the workspace name/ID issuse separately, but it does need fixing.

OK, i'll have a follow-up PR for that, maybe with separate issue? @twisterdotcom

@trjExpensify trjExpensify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Go to any chat
Click Search icon or CMD+K
Verify search input is pre-populated with type:chat in:currentReport

Woah, @twisterdotcom @dannymcclain. I do not think we want to do that. That'll make chat switching incredibly cumbersome by having to clear the contents of the router first.

it's just confusing why the cmd+k popup doesn't show Search in #admins and does show Search in CFO Expensive Pie which is the display name of this current user.

^^ My understanding is this is the bug to focus on.

@dannymcclain

Copy link
Copy Markdown
Contributor

Woah, @twisterdotcom @dannymcclain. I do not think we want to do that. That'll make chat switching incredibly cumbersome by having to clear the contents of the router first.

Totally agree. cc @Expensify/design

it's just confusing why the cmd+k popup doesn't show Search in #admins and does show Search in CFO Expensive Pie which is the display name of this current user.

^^ My understanding is this is the bug to focus on.

Also agree. I think we should also change the default keyboard focus when opening the router—you can see here how I tried to hit command + k and then just arrow down to the Search in {room} option, but because we default focus some option farther down in the list, it feels really wacky.

CleanShot.2026-07-23.at.10.24.23.mp4

Ideally, I could hit command + k, arrow down with the keyboard one time, hit enter, and type my search for the current chat. I think that would be a pretty good experience for this use case.

@mukhrr

mukhrr commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

well to be more clear, we are regretting the feauture asked in original issue and found a new bug: cc @marufsharifi

you can see here how I tried to hit command + k and then just arrow down to the Search in {room} option, but because we default focus some option farther down in the list, it feels really wacky.

CleanShot.2026-07-23.at.10.24.23.mp4
Ideally, I could hit command + k, arrow down with the keyboard one time, hit enter, and type my search for the current chat. I think that would be a pretty good experience for this use case.

@mukhrr

mukhrr commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

found a new bug

And here is:

RCA

Focus skips past the contextual suggestion because SearchAutocompleteList deliberately aims initial focus at the first Recent chats row.

1. The target index is computed by walking sections to the first recent report:

// Locate the first recent report row in the order it is actually rendered. The two-section switcher sorts the
// local "Recent chats" rows by a frozen rank, so the rendered order can differ from styledRecentReports (the
// unsorted combined local + server list). Walking sections keeps the focused row, its reference text, and the
// initially focused key all pointing at the first row the user actually sees.
const recentReportKeys = new Set(styledRecentReports.map((report) => report.keyForList));
let firstRecentReportKey: string | undefined;
let firstRecentReportText = '';
let firstRecentReportFlatIndex = -1;
let flatIndex = 0;
for (const section of sections) {
const hasData = (section.data?.length ?? 0) > 0;
const hasHeader = hasData && (section.title !== undefined || ('customHeader' in section && section.customHeader !== undefined));
if (hasHeader) {
flatIndex++;
}
for (const item of section.data ?? []) {
if (item.keyForList && recentReportKeys.has(item.keyForList)) {
firstRecentReportKey = item.keyForList;
firstRecentReportText = item.text ?? '';
firstRecentReportFlatIndex = flatIndex;
break;
}
flatIndex++;
}
if (firstRecentReportFlatIndex !== -1) {
break;
}
}

2. That index is applied declaratively via initiallyFocusedItemKey:

initiallyFocusedItemKey={!shouldUseNarrowLayout ? firstRecentReportKey : undefined}

3. And again imperatively, because late-arriving options don't pick up initiallyFocusedItemKey:

// When options initialize after the list is already mounted, initiallyFocusedItemKey has no effect
// because useState(initialFocusedIndex) in useArrowKeyFocusManager only reads the initial value.
// Imperatively focus the first recent report once options become available (desktop only).
useEffect(() => {
if (shouldUseNarrowLayout || isLoadingOptions || hasSetInitialFocusRef.current || firstRecentReportFlatIndex === -1) {
return;
}
hasSetInitialFocusRef.current = true;
innerListRef.current?.updateAndScrollToFocusedIndex(firstRecentReportFlatIndex, false);
}, [isLoadingOptions, firstRecentReportFlatIndex, shouldUseNarrowLayout]);

Neither path is aware that a contextual suggestion sits at index 0, so both land on the first recent chat.

Why initiallyFocusedItemKey is the lever: useFlattenedSections starts at -1 and only overwrites when an item key matches, so passing undefined leaves the list unfocused.

if (item.keyForList === initiallyFocusedItemKey && focusedIndex === -1) {

That value feeds the keyboard-focus manager:

const {flattenedData, disabledIndexes, itemsCount, selectedItems, initialFocusedIndex, firstFocusableIndex} = useFlattenedSections(sections, initiallyFocusedItemKey);
const listRef = useRef<FlashListRef<FlattenedItem<ListItem>> | null>(null);
const {scrollToIndex, debouncedScrollToIndex} = useSelectionListScroll(listRef, flattenedData);
const {containerRef, trackScrollOffset, scrollInputIntoView} = useScrollToFocusedInput(listRef, isKeyboardShown);
const {focusedIndex, setFocusedIndex, isKeyboardNavigating, setHasKeyBeenPressed} = useSelectionListKeyboardFocus({
initialFocusedIndex,

Solution

Derive hasContextualSuggestion in the sections useMemo, then suppress both focus paths when it's true — leaving initialFocusedIndex at -1 so the first ArrowDown lands on the suggestion:

initiallyFocusedItemKey = !shouldUseNarrowLayout && !hasContextualSuggestion ? firstRecentReportKey : undefined
// and add hasContextualSuggestion to the early-return guard of the imperative effect

Gives exactly the requested flow: cmd+K → ArrowDown → Enter → type.

Tradeoff needing sign-off

With focus unset, cmd+K → Enter no longer opens the most recent chatgetFocusedItem bails when focusedIndex < 0:

const getFocusedItem = useCallback((): ListItem | undefined => {
if (focusedIndex < 0 || focusedIndex >= flattenedData.length) {
return;
}
const item = flattenedData.at(focusedIndex);
if (!item || shouldTreatItemAsDisabled(item)) {
return;
}
return item as ListItem;
}, [flattenedData, focusedIndex]);

So are we okay with that? @trjExpensify @twisterdotcom @shawnborton @dannymcclain

@dannymcclain

Copy link
Copy Markdown
Contributor

Tradeoff needing sign-off
With focus unset, cmd+K → Enter no longer opens the most recent chat — getFocusedItem bails when focusedIndex < 0:

Personally I would be ok with this - but I'd like to hear from more of the team. Also, when opening the router from a chat room, could we focus that first item (Search in {roomName}) so that you could just do command + k, then enter, then type your query? (Eliminating the need to key down to it?) Definitely definitely curious for more thoughts here.

@twisterdotcom

Copy link
Copy Markdown
Contributor

could we focus that first item (Search in {roomName}) so that you could just do command + k, then enter, then type your query?

Hmm yes okay I like this.

The list focused the first recent chat on open, so reaching "Search in
<chat>" meant arrowing up past every row above it. Focus that suggestion
instead when it is shown, letting Enter scope the search to the chat the
user is already looking at.

Move the section walk into a helper so the flat index, which has to
count header rows, can be tested on its own.
@mukhrr

mukhrr commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

could we focus that first item (Search in {roomName}) so that you could just do command + k, then enter, then type your query?

well that makes sense. Let me implement it

@mukhrr

mukhrr commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

@marufsharifi I think now your previous reviews are obsolete since I had to revert search input pre-population logic. Can you, pls, re-review this? thanks

@mukhrr
mukhrr requested a review from marufsharifi July 24, 2026 00:55
@marufsharifi

marufsharifi commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
Android: mWeb Chrome
Screen_Recording_20260724_124733_Chrome.mp4
iOS: HybridApp
Screen.Recording.2026-07-24.at.3.09.55.PM.mov
iOS: mWeb Safari
Screen.Recording.2026-07-24.at.12.50.16.PM.mov
MacOS: Chrome / Safari
Screen.Recording.2026-07-24.at.12.30.39.PM.mov

@dannymcclain

dannymcclain commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Let us know when this is ready for an ad hoc build - would love for @twisterdotcom, @trjExpensify, and @Expensify/design to get to test it for real.

@marufsharifi

Copy link
Copy Markdown
Contributor

@mukhrr, could you please merge the main. thanks.

@mukhrr

mukhrr commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Let us know when this is ready for an ad hoc build - would love for @twisterdotcom, @trjExpensify, and @Expensify/design to get to test it for real.

@dannymcclain I think this can already be adhoc built. WDYT, @marufsharifi ?

@dubielzyk-expensify

Copy link
Copy Markdown
Contributor

I'll run the adhoc machine 👍

@github-actions

Copy link
Copy Markdown
Contributor

🚧 dubielzyk-expensify has triggered a test Expensify/App build. You can view the workflow run here.

@dubielzyk-expensify

Copy link
Copy Markdown
Contributor

Is it expected that you have to click enter to search in the room? It feels a bit odd, but I guess it's expected to default to global search. Took me a try to understand though.

CleanShot.2026-07-27.at.14.16.26.mp4

@marufsharifi

Copy link
Copy Markdown
Contributor

@trjExpensify, @twisterdotcom, gentle bump on this #96448 (comment). thanks.

@twisterdotcom

Copy link
Copy Markdown
Contributor

You just want us to take a look at the ad-hoc @marufsharifi?

@dannymcclain

Copy link
Copy Markdown
Contributor

Is it expected that you have to click enter to search in the room? It feels a bit odd, but I guess it's expected to default to global search. Took me a try to understand though.

Yeah, that's expected for the build. Instead of taking over global search entirely (which would make chat switching cumbersome), the idea was to just focus the search in: item so if you wanted to search in the room you could just hit enter (or select that first list item - same thing) and off you go. Do you have any other suggestions?

My only other thought for an alt is to add a secondary keyboard shortcut that automatically starts with the search in query already populated in the input (this is how Slack does it: command + k takes you to global search, command + f focuses the search on the chat you're in).

But if we don't want to do something like that, I think the build is pretty good. Definitely curious for others' thoughts.

@twisterdotcom

Copy link
Copy Markdown
Contributor

command + f focuses the search on the chat you're in

This is a setting they have, so not the default, which I only found out recently too: https://expensify.slack.com/archives/C03U7DCU4/p1784070488097879?thread_ts=1784069966.021469&cid=C03U7DCU4

@shawnborton

Copy link
Copy Markdown
Contributor

But if we don't want to do something like that, I think the build is pretty good.

This is generally how I'm feeling too, I guess it feels pretty familiar based on how other apps handle it at least?

@dubielzyk-expensify

Copy link
Copy Markdown
Contributor

Sounds good to me 👍

const personalDetailsOption = createOptionFromReport(report, PERSONAL_DETAILS, undefined, POLICY, sortedActions, undefined, {showPersonalDetails: true});

expect(roomOption.text).toBe('#admins');
expect(personalDetailsOption.text).not.toBe('#admins');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tighten this assertion — it only proves "changed", not "changed to the right thing".

expect(personalDetailsOption.text).not.toBe('#admins');

This passes for any non-#admins value, including an accidental '' or undefined. Assert the actual expected participant display name so a future regression that produces an empty string is caught:

expect(personalDetailsOption.text).toBe(/* expected participant display name */);

@marufsharifi

Copy link
Copy Markdown
Contributor

@MelvinBot review

@MelvinBot

Copy link
Copy Markdown
Contributor

Review

Overall this looks solid and correctly implements both the room-name fix and the "focus the Search in <room> suggestion" feature agreed on in the thread. Both changes are small, well-commented, and covered by tests, and all CI checks pass. A few points below — only one is worth acting on before merge.

1. Weak test assertion (should tighten before merge) — Already flagged in this earlier comment and still unaddressed. In tests/unit/OptionsListUtilsTest.tsx:8491:

expect(personalDetailsOption.text).not.toBe('#admins');

This passes for any non-#admins value, including an accidental '' or undefined — so it wouldn't catch a regression that produces an empty name. Assert the actual expected participant display name instead.

2. showPersonalDetails: isOneOnOneChat(...) is the right call ✅ — Verified the mechanism: with showPersonalDetails: false, createOption falls back to computedReportName (the room name), and passing reportAttributes is what makes deprecatedGetReportName resolve it correctly. isOneOnOneChat returns false for rooms, policy-expense chats, and group chats, so those all show the report/room name, while true 1:1 DMs still show the person's name. This is actually an improvement over the old showPersonalDetails: true for group chats too (which previously showed just one participant's name).

3. getAutocompleteInitialFocus — minor ordering assumption (non-blocking) — In getAutocompleteInitialFocus.ts:50-60, the loop breaks as soon as it finds the first recent report. This is correct only because the contextual suggestion always renders in an earlier section than the recent chats. If a recent report ever preceded the suggestion in flat order, the suggestion would never be detected and focus would fall back to the recent report. Given today's section layout this is safe — just noting it as an implicit contract. The refactor otherwise faithfully preserves the original header-counting logic, and the new test file covers the header/empty-section/no-focus cases well.

Verification notes
  • Confirmed createOptionFromReport's 6th arg is reportAttributesDerived and 7th is config — the diff passes reportAttributes and {showPersonalDetails} into the correct positions.
  • Confirmed reportAttributes comes from useReportAttributes() and is already in the memo dependency array.
  • Traced getAutocompleteInitialFocus flat-index math against all three unit tests (contextual-first, header-counting, empty-section) — indices are correct.
  • The 'customHeader' in sectionsection.customHeader !== undefined change is behaviorally equivalent.
  • The type-to-highlight effect still correctly targets firstRecentReportFlatIndex (not the new default), so typing still highlights the first recent chat.

The earlier review threads about SearchRouterUtils seeding / getContextualSearchTarget appear obsolete since the input pre-population was reverted, and the workspace-name-with-comma/space sanitize issue is already agreed to be a separate follow-up. Only item #1 needs addressing here.

@marufsharifi

Copy link
Copy Markdown
Contributor

@mukhrr, could you please check the above comments? thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants