From f60a7190ed1d3eaa072a321649d87c40c7da8ba4 Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Mon, 9 Feb 2026 15:40:27 +0100 Subject: [PATCH 01/10] Make NewChatPage use new SelectionListWithSections --- .../BaseSelectionListWithSections.tsx | 3 +- .../SelectionListWithSections/types.ts | 1 + src/pages/NewChatPage.tsx | 63 ++++++++++--------- 3 files changed, 37 insertions(+), 30 deletions(-) diff --git a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx index 3237d0760fb8a..738d886f931dc 100644 --- a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx +++ b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx @@ -177,8 +177,9 @@ function BaseSelectionListWithSections({ ref, () => ({ focusTextInput, + scrollToIndex, }), - [focusTextInput], + [focusTextInput, scrollToIndex], ); // Disable `Enter` shortcut if the active element is a button or checkbox diff --git a/src/components/SelectionList/SelectionListWithSections/types.ts b/src/components/SelectionList/SelectionListWithSections/types.ts index 30eebec862a31..f4b7830f86991 100644 --- a/src/components/SelectionList/SelectionListWithSections/types.ts +++ b/src/components/SelectionList/SelectionListWithSections/types.ts @@ -43,6 +43,7 @@ type SelectionListWithSectionsProps = BaseSelectionListP type SelectionListWithSectionsHandle = { focusTextInput: () => void; + scrollToIndex: (index: number) => void; }; type SectionHeader = { diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 4d0b3c00bc77f..62cc2eda1a6fb 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -1,6 +1,5 @@ import {useFocusEffect} from '@react-navigation/native'; import reportsSelector from '@selectors/Attributes'; -import isEmpty from 'lodash/isEmpty'; import reject from 'lodash/reject'; import type {Ref} from 'react'; import React, {useEffect, useImperativeHandle, useRef, useState} from 'react'; @@ -11,10 +10,10 @@ import {PressableWithFeedback} from '@components/Pressable'; import ReferralProgramCTA from '@components/ReferralProgramCTA'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectCircle from '@components/SelectCircle'; -// eslint-disable-next-line no-restricted-imports -import SelectionList from '@components/SelectionListWithSections'; -import type {ListItem, SelectionListHandle} from '@components/SelectionListWithSections/types'; -import UserListItem from '@components/SelectionListWithSections/UserListItem'; +import UserListItem from '@components/SelectionList/ListItem/UserListItem'; +import SelectionListWithSections from '@components/SelectionList/SelectionListWithSections'; +import {Section} from '@components/SelectionList/SelectionListWithSections/types'; +import type {ListItem, SelectionListWithSectionsHandle} from '@components/SelectionList/types'; import useContactImport from '@hooks/useContactImport'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDebouncedState from '@hooks/useDebouncedState'; @@ -30,7 +29,6 @@ import {navigateToAndOpenReport, searchInServer, setGroupDraft} from '@libs/acti import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; -import type {Option, Section} from '@libs/OptionsListUtils'; import { filterAndOrderOptions, filterSelectedOptions, @@ -41,12 +39,14 @@ import { getUserToInviteOption, getValidOptions, } from '@libs/OptionsListUtils'; +import {OptionWithKey} from '@libs/OptionsListUtils/types'; import type {OptionData} from '@libs/ReportUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {SelectedParticipant} from '@src/types/onyx/NewGroupChatDraft'; +import getEmptyArray from '@src/types/utils/getEmptyArray'; import KeyboardUtils from '@src/utils/keyboard'; const excludedGroupEmails = new Set(CONST.EXPENSIFY_EMAILS.filter((value) => value !== CONST.EMAIL.CONCIERGE)); @@ -239,7 +239,7 @@ function NewChatPage({ref}: NewChatPageProps) { const {top} = useSafeAreaInsets(); const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false, canBeMissing: true}); const [reportAttributesDerived] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {canBeMissing: true, selector: reportsSelector}); - const selectionListRef = useRef(null); + const selectionListRef = useRef(null); const allPersonalDetails = usePersonalDetails(); const {singleExecution} = useSingleExecution(); @@ -263,7 +263,7 @@ function NewChatPage({ref}: NewChatPageProps) { areOptionsInitialized, } = useOptions(); - const sections: Section[] = []; + const sections: Section[] = []; let firstKeyForList = ''; const formatResults = formatSectionsFromSearchTerm( @@ -277,9 +277,7 @@ function NewChatPage({ref}: NewChatPageProps) { undefined, reportAttributesDerived, ); - // Just a temporary fix to satisfy the type checker - // Will be fixed when migrating to use new SelectionListWithSections - sections.push({...formatResults.section, title: undefined, shouldShow: true}); + sections.push({...formatResults.section, title: undefined, sectionIndex: 0}); if (!firstKeyForList) { firstKeyForList = getFirstKeyForList(formatResults.section.data); @@ -288,7 +286,7 @@ function NewChatPage({ref}: NewChatPageProps) { sections.push({ title: translate('common.recents'), data: selectedOptions.length ? recentReports.filter((option) => !option.isSelfDM) : recentReports, - shouldShow: !isEmpty(recentReports), + sectionIndex: 1, }); if (!firstKeyForList) { firstKeyForList = getFirstKeyForList(recentReports); @@ -297,7 +295,7 @@ function NewChatPage({ref}: NewChatPageProps) { sections.push({ title: translate('common.contacts'), data: personalDetails, - shouldShow: !isEmpty(personalDetails), + sectionIndex: 2, }); if (!firstKeyForList) { firstKeyForList = getFirstKeyForList(personalDetails); @@ -307,7 +305,7 @@ function NewChatPage({ref}: NewChatPageProps) { sections.push({ title: undefined, data: [userToInvite], - shouldShow: true, + sectionIndex: 3, }); if (!firstKeyForList) { firstKeyForList = getFirstKeyForList([userToInvite]); @@ -326,10 +324,10 @@ function NewChatPage({ref}: NewChatPageProps) { newSelectedOptions = reject(selectedOptions, (selectedOption) => selectedOption.login === option.login); } else { newSelectedOptions = [...selectedOptions, {...option, isSelected: true, selected: true, reportID: option.reportID, keyForList: `${option.keyForList ?? option.reportID}`}]; - selectionListRef?.current?.scrollToIndex(0, true); + selectionListRef?.current?.scrollToIndex(0); } - selectionListRef?.current?.clearInputAfterSelect?.(); + // selectionListRef?.current?.clearInputAfterSelect?.(); if (!canUseTouchScreen()) { selectionListRef.current?.focusTextInput(); } @@ -355,7 +353,7 @@ function NewChatPage({ref}: NewChatPageProps) { * creates a new 1:1 chat with the option and the current user, * or navigates to the existing chat if one with those participants already exists. */ - const selectOption = (option?: Option) => { + const selectOption = (option?: OptionWithKey) => { if (option?.isSelfDM) { if (!option.reportID) { Navigation.dismissModal(); @@ -399,7 +397,7 @@ function NewChatPage({ref}: NewChatPageProps) { }); }; - const itemRightSideComponent = (item: ListItem & Option, isFocused?: boolean) => { + const itemRightSideComponent = (item: OptionWithKey, isFocused?: boolean) => { if (!!item.isSelfDM || (item.login && excludedGroupEmails.has(item.login)) || !item.login) { return null; } @@ -468,6 +466,15 @@ function NewChatPage({ref}: NewChatPageProps) { ); + const textInputOptions = { + label: translate('selectionList.nameEmailOrPhoneNumber'), + hint: isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : '', + value: searchTerm, + onChangeText: setSearchTerm, + headerMessage, + disableAutoFocus: true, + }; + return ( - + ref={selectionListRef} ListItem={UserListItem} - sections={areOptionsInitialized ? sections : CONST.EMPTY_ARRAY} - textInputValue={searchTerm} - textInputHint={isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''} - onChangeText={setSearchTerm} - textInputLabel={translate('selectionList.nameEmailOrPhoneNumber')} - headerMessage={headerMessage} + sections={areOptionsInitialized ? sections : getEmptyArray>()} onSelectRow={selectOption} + shouldShowTextInput + textInputOptions={textInputOptions} shouldSingleExecuteRowSelect - onConfirm={(e, option) => (selectedOptions.length > 0 ? createGroup() : selectOption(option))} + initiallyFocusedItemKey={firstKeyForList} + confirmButtonOptions={{ + onConfirm: (e, option) => (selectedOptions.length > 0 ? createGroup() : selectOption(option)), + }} rightHandSideComponent={itemRightSideComponent} footerContent={footerContent} showLoadingPlaceholder={!areOptionsInitialized} @@ -499,10 +506,8 @@ function NewChatPage({ref}: NewChatPageProps) { isLoadingNewOptions={!!isSearchingForReports || isLoadingMore} onEndReached={handleEndReached} onEndReachedThreshold={0.75} - initiallyFocusedOptionKey={firstKeyForList} - shouldTextInputInterceptSwipe + // shouldTextInputInterceptSwipe addBottomSafeAreaPadding - textInputAutoFocus={false} /> ); From d1233606286e135caef0de3d531af756bee35fd6 Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Mon, 9 Feb 2026 15:51:04 +0100 Subject: [PATCH 02/10] Fix lint --- src/pages/NewChatPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 62cc2eda1a6fb..7435431d75a3f 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -12,7 +12,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import SelectCircle from '@components/SelectCircle'; import UserListItem from '@components/SelectionList/ListItem/UserListItem'; import SelectionListWithSections from '@components/SelectionList/SelectionListWithSections'; -import {Section} from '@components/SelectionList/SelectionListWithSections/types'; +import type {Section} from '@components/SelectionList/SelectionListWithSections/types'; import type {ListItem, SelectionListWithSectionsHandle} from '@components/SelectionList/types'; import useContactImport from '@hooks/useContactImport'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -39,7 +39,7 @@ import { getUserToInviteOption, getValidOptions, } from '@libs/OptionsListUtils'; -import {OptionWithKey} from '@libs/OptionsListUtils/types'; +import type {OptionWithKey} from '@libs/OptionsListUtils/types'; import type {OptionData} from '@libs/ReportUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -263,7 +263,7 @@ function NewChatPage({ref}: NewChatPageProps) { areOptionsInitialized, } = useOptions(); - const sections: Section[] = []; + const sections: Array> = []; let firstKeyForList = ''; const formatResults = formatSectionsFromSearchTerm( From 72fde08fe6f786566bdd95f817fc98dfd67a8814 Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Tue, 10 Feb 2026 12:50:43 +0100 Subject: [PATCH 03/10] Fix scrollint --- src/pages/NewChatPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index fe55785859aa6..e0c74392ad10c 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -509,6 +509,7 @@ function NewChatPage({ref}: NewChatPageProps) { isLoadingNewOptions={!!isSearchingForReports || isLoadingMore} onEndReached={handleEndReached} onEndReachedThreshold={0.75} + disableMaintainingScrollPosition // shouldTextInputInterceptSwipe addBottomSafeAreaPadding /> From 67d8f5fe75d0334a70deff95b63fea7abc3ab59e Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Tue, 10 Feb 2026 14:14:43 +0100 Subject: [PATCH 04/10] Adjustments --- src/components/SelectionList/components/TextInput.tsx | 4 ++-- src/components/SelectionList/types.ts | 3 +++ src/pages/NewChatPage.tsx | 2 +- tests/ui/NewChatPageTest.tsx | 7 ++++--- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/SelectionList/components/TextInput.tsx b/src/components/SelectionList/components/TextInput.tsx index ae4e2548541af..5a0dc5c331ac3 100644 --- a/src/components/SelectionList/components/TextInput.tsx +++ b/src/components/SelectionList/components/TextInput.tsx @@ -65,7 +65,7 @@ function TextInput({ }: TextInputProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const {label, value, onChangeText, errorText, headerMessage, hint, disableAutoFocus, placeholder, maxLength, inputMode, ref: optionsRef, style} = options ?? {}; + const {label, value, onChangeText, errorText, headerMessage, hint, disableAutoFocus, placeholder, maxLength, inputMode, ref: optionsRef, style, shouldInterceptSwipe} = options ?? {}; const resultsFound = headerMessage !== translate('common.noResultsFound'); const noData = dataLength === 0 && !showLoadingPlaceholder; const shouldShowHeaderMessage = !!headerMessage && (!isLoadingNewOptions || resultsFound || noData); @@ -134,7 +134,7 @@ function TextInput({ isLoading={isLoading} testID="selection-list-text-input" errorText={errorText} - shouldInterceptSwipe={false} + shouldInterceptSwipe={shouldInterceptSwipe ?? false} /> {shouldShowHeaderMessage && ( diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 820511f5619a3..6b31a6155a20e 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -238,6 +238,9 @@ type TextInputOptions = { /** Whether the text input autofocus should be disabled */ disableAutoFocus?: boolean; + /** Whether the text input should intercept swipes */ + shouldInterceptSwipe?: boolean; + /** Styles for the text input */ style?: { /** Styles for the text input container */ diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index e0c74392ad10c..efb4aa4ae48d6 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -476,6 +476,7 @@ function NewChatPage({ref}: NewChatPageProps) { onChangeText: setSearchTerm, headerMessage, disableAutoFocus: true, + shouldInterceptSwipe: true, }; return ( @@ -510,7 +511,6 @@ function NewChatPage({ref}: NewChatPageProps) { onEndReached={handleEndReached} onEndReachedThreshold={0.75} disableMaintainingScrollPosition - // shouldTextInputInterceptSwipe addBottomSafeAreaPadding /> diff --git a/tests/ui/NewChatPageTest.tsx b/tests/ui/NewChatPageTest.tsx index d1a55f210e9a7..5f7686834b4ce 100644 --- a/tests/ui/NewChatPageTest.tsx +++ b/tests/ui/NewChatPageTest.tsx @@ -1,7 +1,8 @@ import * as NativeNavigation from '@react-navigation/native'; import {act, fireEvent, render, screen, waitFor, within} from '@testing-library/react-native'; import React from 'react'; -import {SectionList} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import {ScrollView} from 'react-native'; import Onyx from 'react-native-onyx'; import HTMLEngineProvider from '@components/HTMLEngineProvider'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; @@ -77,11 +78,11 @@ describe('NewChatPage', () => { act(() => { (NativeNavigation as NativeNavigationMock).triggerTransitionEnd(); }); - const spy = jest.spyOn(SectionList.prototype, 'scrollToLocation'); + const scrollToSpy = jest.spyOn(ScrollView.prototype, 'scrollTo'); const addButton = await waitFor(() => screen.getAllByText(translateLocal('newChatPage.addToGroup')).at(0)); if (addButton) { fireEvent.press(addButton); - expect(spy).toHaveBeenCalledWith(expect.objectContaining({itemIndex: 0})); + expect(scrollToSpy).toHaveBeenCalled(); } }); From 19524248d534fc372f48afe6a9b6a54f166e29b5 Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Wed, 11 Feb 2026 10:20:06 +0100 Subject: [PATCH 05/10] Adjustments --- src/libs/OptionsListUtils/index.ts | 14 -------------- src/pages/NewChatPage.tsx | 16 ---------------- 2 files changed, 30 deletions(-) diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 3ba2fee6c162b..b8f0d2edb4121 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -2802,19 +2802,6 @@ function formatSectionsFromSearchTerm( }; } -/** - * Helper method to get the `keyForList` for the first option in the OptionsList - */ -function getFirstKeyForList(data?: Option[] | null) { - if (!data?.length) { - return ''; - } - - const firstNonEmptyDataObj = data.at(0); - - return firstNonEmptyDataObj?.keyForList ? firstNonEmptyDataObj?.keyForList : ''; -} - function getPersonalDetailSearchTerms(item: Partial, currentUserAccountID: number) { if (item.accountID === currentUserAccountID) { return getCurrentUserSearchTerms(item); @@ -3209,7 +3196,6 @@ export { getFilteredRecentAttendees, getCurrentUserSearchTerms, getEmptyOptions, - getFirstKeyForList, getHeaderMessage, getHeaderMessageForNonUserList, getIOUConfirmationOptionsFromPayeePersonalDetail, diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index efb4aa4ae48d6..6a0e94873d942 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -32,7 +32,6 @@ import { filterAndOrderOptions, filterSelectedOptions, formatSectionsFromSearchTerm, - getFirstKeyForList, getHeaderMessage, getPersonalDetailSearchTerms, getUserToInviteOption, @@ -267,7 +266,6 @@ function NewChatPage({ref}: NewChatPageProps) { } = useOptions(reportAttributesDerived); const sections: Array> = []; - let firstKeyForList = ''; const formatResults = formatSectionsFromSearchTerm( debouncedSearchTerm, @@ -282,27 +280,17 @@ function NewChatPage({ref}: NewChatPageProps) { ); sections.push({...formatResults.section, title: undefined, sectionIndex: 0}); - if (!firstKeyForList) { - firstKeyForList = getFirstKeyForList(formatResults.section.data); - } - sections.push({ title: translate('common.recents'), data: selectedOptions.length ? recentReports.filter((option) => !option.isSelfDM) : recentReports, sectionIndex: 1, }); - if (!firstKeyForList) { - firstKeyForList = getFirstKeyForList(recentReports); - } sections.push({ title: translate('common.contacts'), data: personalDetails, sectionIndex: 2, }); - if (!firstKeyForList) { - firstKeyForList = getFirstKeyForList(personalDetails); - } if (userToInvite) { sections.push({ @@ -310,9 +298,6 @@ function NewChatPage({ref}: NewChatPageProps) { data: [userToInvite], sectionIndex: 3, }); - if (!firstKeyForList) { - firstKeyForList = getFirstKeyForList([userToInvite]); - } } /** @@ -499,7 +484,6 @@ function NewChatPage({ref}: NewChatPageProps) { shouldShowTextInput textInputOptions={textInputOptions} shouldSingleExecuteRowSelect - initiallyFocusedItemKey={firstKeyForList} confirmButtonOptions={{ onConfirm: (e, option) => (selectedOptions.length > 0 ? createGroup() : selectOption(option)), }} From 58084771b9d14f6b07a1e6a1be6ae27c2ce4dc02 Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Thu, 12 Feb 2026 15:55:14 +0100 Subject: [PATCH 06/10] Add clear focus handling --- .../BaseSelectionListWithSections.tsx | 44 +++++++++++-------- .../SelectionListWithSections/types.ts | 1 + src/pages/NewChatPage.tsx | 2 +- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx index 4407080cddd12..66d693888ec52 100644 --- a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx +++ b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx @@ -92,23 +92,26 @@ function BaseSelectionListWithSections({ hasKeyBeenPressed.current = true; }; - const scrollToIndex = (index: number) => { - if (index < 0 || index >= flattenedData.length || !listRef.current) { - return; - } - const item = flattenedData.at(index); - if (!item) { - return; - } - try { - listRef.current.scrollToIndex({index}); - } catch (error) { - // FlashList may throw if layout for this index doesn't exist yet - // This can happen when data changes rapidly (e.g., during search filtering) - // The layout will be computed on next render, so we can safely ignore this - Log.warn('SelectionListWithSections: error scrolling to index', {error}); - } - }; + const scrollToIndex = useCallback( + (index: number) => { + if (index < 0 || index >= flattenedData.length || !listRef.current) { + return; + } + const item = flattenedData.at(index); + if (!item) { + return; + } + try { + listRef.current.scrollToIndex({index}); + } catch (error) { + // FlashList may throw if layout for this index doesn't exist yet + // This can happen when data changes rapidly (e.g., during search filtering) + // The layout will be computed on next render, so we can safely ignore this + Log.warn('SelectionListWithSections: error scrolling to index', {error}); + } + }, + [flattenedData], + ); const debouncedScrollToIndex = useDebounce(scrollToIndex, CONST.TIMING.LIST_SCROLLING_DEBOUNCE_TIME, {leading: true, trailing: true}); @@ -174,13 +177,18 @@ function BaseSelectionListWithSections({ innerTextInputRef.current?.focus(); }, []); + const clearInputAfterSelect = useCallback(() => { + textInputOptions?.onChangeText?.(''); + }, [textInputOptions]); + useImperativeHandle( ref, () => ({ focusTextInput, scrollToIndex, + clearInputAfterSelect, }), - [focusTextInput, scrollToIndex], + [focusTextInput, scrollToIndex, clearInputAfterSelect], ); // Disable `Enter` shortcut if the active element is a button or checkbox diff --git a/src/components/SelectionList/SelectionListWithSections/types.ts b/src/components/SelectionList/SelectionListWithSections/types.ts index aae1490f7df0d..9281545e48b9a 100644 --- a/src/components/SelectionList/SelectionListWithSections/types.ts +++ b/src/components/SelectionList/SelectionListWithSections/types.ts @@ -47,6 +47,7 @@ type SelectionListWithSectionsProps = BaseSelectionListP type SelectionListWithSectionsHandle = { focusTextInput: () => void; scrollToIndex: (index: number) => void; + clearInputAfterSelect: () => void; }; type SectionHeader = { diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 6a0e94873d942..bc3dc71764e0a 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -315,7 +315,7 @@ function NewChatPage({ref}: NewChatPageProps) { selectionListRef?.current?.scrollToIndex(0); } - // selectionListRef?.current?.clearInputAfterSelect?.(); + selectionListRef.current?.clearInputAfterSelect(); if (!canUseTouchScreen()) { selectionListRef.current?.focusTextInput(); } From ed30ff761c34b6f1eaa9213b7afe7d34b83499f9 Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Mon, 16 Feb 2026 11:27:37 +0100 Subject: [PATCH 07/10] Change condition for loading more data --- src/hooks/useFilteredOptions.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/hooks/useFilteredOptions.ts b/src/hooks/useFilteredOptions.ts index ea8fa9af133d9..309e3a7e099fd 100644 --- a/src/hooks/useFilteredOptions.ts +++ b/src/hooks/useFilteredOptions.ts @@ -106,15 +106,11 @@ function useFilteredOptions(config: UseFilteredOptionsConfig = {}): UseFilteredO if (!options || isLoadingMore) { return; } - - const hasMoreToLoad = options.reports.length < totalReports; - if (hasMoreToLoad) { - setIsLoadingMore(true); - setReportsLimit((prev) => prev + batchSize); - } + setIsLoadingMore(true); + setReportsLimit((prev) => prev + batchSize); }; - const hasMore = options ? options.reports.length < totalReports : false; + const hasMore = options ? reportsLimit < totalReports : false; return { options, From 0b028e4012ddceb9a858baec75caeffde3ffe19e Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Mon, 16 Feb 2026 16:05:11 +0100 Subject: [PATCH 08/10] Fix issues --- src/pages/NewChatPage.tsx | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index f6db45d0e6e89..c53e20083de83 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -1,7 +1,7 @@ -import {useFocusEffect} from '@react-navigation/native'; +import {useFocusEffect, useIsFocused} from '@react-navigation/native'; import reject from 'lodash/reject'; import type {Ref} from 'react'; -import React, {useEffect, useImperativeHandle, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; import {Keyboard} from 'react-native'; import Button from '@components/Button'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; @@ -37,6 +37,7 @@ import { getUserToInviteOption, getValidOptions, } from '@libs/OptionsListUtils'; +import reportsSelector from '@selectors/Attributes'; import type {OptionWithKey} from '@libs/OptionsListUtils/types'; import type {OptionData} from '@libs/ReportUtils'; import variables from '@styles/variables'; @@ -70,6 +71,7 @@ function useOptions(reportAttributesDerived: ReportAttributesDerivedValue['repor const {contacts} = useContactImport(); const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); const allPersonalDetails = usePersonalDetails(); + const isScreenFocused = useIsFocused(); const { options: listOptions, @@ -201,7 +203,7 @@ function useOptions(reportAttributesDerived: ReportAttributesDerivedValue['repor }, [draftSelectedOptions, setSelectedOptions]); const handleEndReached = () => { - if (!hasMore || isLoadingMore || !areOptionsInitialized) { + if (!hasMore || isLoadingMore || !areOptionsInitialized || !isScreenFocused) { return; } loadMore(); @@ -240,8 +242,10 @@ function NewChatPage({ref}: NewChatPageProps) { const {top} = useSafeAreaInsets(); const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false, canBeMissing: true}); const selectionListRef = useRef(null); - const [reportAttributesDerivedFull] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {canBeMissing: true}); - const reportAttributesDerived = reportAttributesDerivedFull?.reports; + const [reportAttributesDerived] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, { + canBeMissing: true, + selector: reportsSelector, + }); const allPersonalDetails = usePersonalDetails(); const {singleExecution} = useSingleExecution(); @@ -250,6 +254,25 @@ function NewChatPage({ref}: NewChatPageProps) { focus: selectionListRef.current?.focusTextInput, })); + // Opacity toggle to fix FlashList layout issue when navigating back. + // FlashList compacts its layout when the screen is hidden but still mounted. + // Briefly setting opacity to 0 when the screen regains focus triggers a re-render + // that forces FlashList to recalculate its layout without a full remount. + const [listOpacity, setListOpacity] = useState(1); + const isFirstRender = useRef(true); + useFocusEffect( + useCallback(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + setListOpacity(0); + requestAnimationFrame(() => { + setListOpacity(1); + }); + }, []), + ); + const { headerMessage, searchTerm, @@ -496,6 +519,7 @@ function NewChatPage({ref}: NewChatPageProps) { onEndReachedThreshold={0.75} disableMaintainingScrollPosition addBottomSafeAreaPadding + style={{listStyle: {opacity: listOpacity}}} /> ); From fb83622254ea63ef66d2a9c0ea0ed5272cd0ac66 Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Wed, 18 Feb 2026 15:49:38 +0100 Subject: [PATCH 09/10] Fix layout issue on web --- src/pages/NewChatPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index c53e20083de83..f3577c8f70d60 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -2,7 +2,7 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; import reject from 'lodash/reject'; import type {Ref} from 'react'; import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import {Keyboard} from 'react-native'; +import {Keyboard, Platform} from 'react-native'; import Button from '@components/Button'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; import {PressableWithFeedback} from '@components/Pressable'; @@ -262,7 +262,7 @@ function NewChatPage({ref}: NewChatPageProps) { const isFirstRender = useRef(true); useFocusEffect( useCallback(() => { - if (isFirstRender.current) { + if (isFirstRender.current || Platform.OS !== 'web') { isFirstRender.current = false; return; } From e82c69f59b7111f67512df0f5e3d4952f64c237f Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Thu, 26 Feb 2026 10:39:24 +0100 Subject: [PATCH 10/10] Prettier --- src/pages/NewChatPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 11a50f5d86c1a..b73bb75151957 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -1,4 +1,5 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; +import reportsSelector from '@selectors/Attributes'; import reject from 'lodash/reject'; import type {Ref} from 'react'; import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; @@ -37,7 +38,6 @@ import { getUserToInviteOption, getValidOptions, } from '@libs/OptionsListUtils'; -import reportsSelector from '@selectors/Attributes'; import type {OptionWithKey} from '@libs/OptionsListUtils/types'; import type {OptionData} from '@libs/ReportUtils'; import variables from '@styles/variables';