11import isEmpty from 'lodash/isEmpty' ;
2- import React , { memo , useCallback , useMemo , useRef , useState } from 'react' ;
2+ import React , { memo , useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
33import { View } from 'react-native' ;
44import Button from '@components/Button' ;
55import { usePersonalDetails } from '@components/OnyxListItemProvider' ;
@@ -9,19 +9,16 @@ import type {ListItem, SelectionListHandle} from '@components/SelectionList/type
99import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' ;
1010import useLocalize from '@hooks/useLocalize' ;
1111import useOnyx from '@hooks/useOnyx' ;
12- import usePersonalDetailOptions from '@hooks/usePersonalDetailOptions' ;
1312import useResponsiveLayout from '@hooks/useResponsiveLayout' ;
13+ import useSearchSelector from '@hooks/useSearchSelector' ;
1414import useThemeStyles from '@hooks/useThemeStyles' ;
1515import useWindowDimensions from '@hooks/useWindowDimensions' ;
1616import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus' ;
17- import memoize from '@libs/memoize' ;
18- import { filterOption , getValidOptions } from '@libs/PersonalDetailOptionsListUtils' ;
19- import type { OptionData } from '@libs/PersonalDetailOptionsListUtils' ;
17+ import { getParticipantsOption } from '@libs/OptionsListUtils' ;
18+ import type { OptionData } from '@libs/ReportUtils' ;
2019import CONST from '@src/CONST' ;
2120import ONYXKEYS from '@src/ONYXKEYS' ;
2221
23- const memoizedGetValidOptions = memoize ( getValidOptions , { maxSize : 5 , monitoringName : 'UserSelectPopup.getValidOptions' } ) ;
24-
2522type UserSelectPopupProps = {
2623 /** The currently selected users */
2724 value : string [ ] ;
@@ -43,75 +40,82 @@ type UserSelectPopupProps = {
4340function UserSelectPopup ( { value, closeOverlay, onChange, isSearchable} : UserSelectPopupProps ) {
4441 const selectionListRef = useRef < SelectionListHandle < ListItem > | null > ( null ) ;
4542 const styles = useThemeStyles ( ) ;
46- const { translate, formatPhoneNumber} = useLocalize ( ) ;
47- const { options, currentOption} = usePersonalDetailOptions ( ) ;
43+ const { translate} = useLocalize ( ) ;
4844 const personalDetails = usePersonalDetails ( ) ;
4945 const { windowHeight} = useWindowDimensions ( ) ;
5046 const { shouldUseNarrowLayout} = useResponsiveLayout ( ) ;
5147 const currentUserPersonalDetails = useCurrentUserPersonalDetails ( ) ;
52- const currentUserEmail = currentUserPersonalDetails . email ?? '' ;
48+ const currentUserAccountID = currentUserPersonalDetails . accountID ;
5349 const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus ( ) ;
54- const [ countryCode = CONST . DEFAULT_COUNTRY_CODE ] = useOnyx ( ONYXKEYS . COUNTRY_CODE ) ;
55- const [ loginList ] = useOnyx ( ONYXKEYS . LOGIN_LIST ) ;
56- const [ searchTerm , setSearchTerm ] = useState ( '' ) ;
5750 const [ isSearchingForReports ] = useOnyx ( ONYXKEYS . IS_SEARCHING_FOR_REPORTS , { initWithStoredValues : false } ) ;
58-
59- const getInitialSelectedIDs = useCallback ( ( ) => {
60- return value . reduce < Set < string > > ( ( acc , id ) => {
51+ const initialSelectedOptions = useMemo ( ( ) => {
52+ return value . reduce < OptionData [ ] > ( ( options , id ) => {
6153 const participant = personalDetails ?. [ id ] ;
6254 if ( ! participant ) {
63- return acc ;
55+ return options ;
6456 }
65- acc . add ( id ) ;
66- return acc ;
67- } , new Set < string > ( ) ) ;
68- } , [ value , personalDetails ] ) ;
6957
70- const [ selectedAccountIDs , setSelectedAccountIDs ] = useState < Set < string > > ( ( ) => getInitialSelectedIDs ( ) ) ;
58+ const optionData = {
59+ ...getParticipantsOption ( participant , personalDetails ) ,
60+ isSelected : true ,
61+ } ;
7162
72- const cleanSearchTerm = searchTerm . trim ( ) . toLowerCase ( ) ;
63+ if ( optionData ) {
64+ options . push ( optionData as OptionData ) ;
65+ }
7366
74- const transformedOptions = useMemo (
75- ( ) =>
76- options ?. map ( ( option ) => ( {
77- ...option ,
78- isSelected : selectedAccountIDs . has ( option . accountID . toString ( ) ) ,
79- } ) ) ?? [ ] ,
80- [ options , selectedAccountIDs ] ,
81- ) ;
67+ return options ;
68+ } , [ ] ) ;
69+ } , [ value , personalDetails ] ) ;
8270
83- const optionsList = useMemo ( ( ) => {
84- return memoizedGetValidOptions ( transformedOptions , currentUserEmail , formatPhoneNumber , countryCode , loginList , {
71+ const { searchTerm, debouncedSearchTerm, setSearchTerm, availableOptions, selectedOptions, toggleSelection, areOptionsInitialized, selectedOptionsForDisplay, onListEndReached} =
72+ useSearchSelector ( {
73+ selectionMode : CONST . SEARCH_SELECTOR . SELECTION_MODE_MULTI ,
74+ searchContext : CONST . SEARCH_SELECTOR . SEARCH_CONTEXT_GENERAL ,
75+ initialSelected : initialSelectedOptions ,
8576 excludeLogins : CONST . EXPENSIFY_EMAILS_OBJECT ,
86- includeCurrentUser : false ,
87- includeRecentReports : false ,
88- searchString : cleanSearchTerm ,
77+ maxRecentReportsToShow : CONST . IOU . MAX_RECENT_REPORTS_TO_SHOW ,
78+ includeUserToInvite : false ,
79+ includeCurrentUser : true ,
8980 } ) ;
90- } , [ transformedOptions , currentUserEmail , cleanSearchTerm , formatPhoneNumber , countryCode , loginList ] ) ;
9181
92- const currentUserSearchTerms = useMemo ( ( ) => [ translate ( 'common.you' ) , translate ( 'common.me' ) ] , [ translate ] ) ;
82+ const listData = useMemo ( ( ) => {
83+ const personalDetailsList = availableOptions . personalDetails . map ( ( participant ) => ( {
84+ ...participant ,
85+ keyForList : String ( participant . accountID ) ,
86+ } ) ) ;
87+ const recentReports = availableOptions . recentReports . map ( ( report ) => ( {
88+ ...report ,
89+ keyForList : String ( report . reportID ) ,
90+ } ) ) ;
91+ const combinedOptions = [ ...selectedOptionsForDisplay , ...personalDetailsList , ...recentReports ] ;
92+
93+ // Sort the options so that selected items appear first, and the current user appears right after that, followed by the rest of the options in their original order
94+ combinedOptions . sort ( ( a , b ) => {
95+ // selected items first
96+ if ( a . isSelected && ! b . isSelected ) {
97+ return - 1 ;
98+ }
99+ if ( ! a . isSelected && b . isSelected ) {
100+ return 1 ;
101+ }
93102
94- const filteredCurrentUserOption = useMemo ( ( ) => {
95- const newOption = filterOption ( currentOption , cleanSearchTerm , currentUserSearchTerms ) ;
96- if ( newOption ) {
97- return {
98- ...newOption ,
99- isSelected : selectedAccountIDs . has ( newOption . accountID . toString ( ) ) ,
100- } ;
101- }
102- return newOption ;
103- } , [ currentOption , cleanSearchTerm , selectedAccountIDs , currentUserSearchTerms ] ) ;
103+ // Put the current user at the top of the list
104+ if ( a . accountID === currentUserAccountID ) {
105+ return - 1 ;
106+ }
107+ if ( b . accountID === currentUserAccountID ) {
108+ return 1 ;
109+ }
110+ return 0 ;
111+ } ) ;
104112
105- const listData = useMemo ( ( ) => {
106- if ( ! filteredCurrentUserOption ) {
107- return [ ...optionsList . selectedOptions , ...optionsList . personalDetails ] ;
108- }
109- const isCurrentOptionSelected = filteredCurrentUserOption . isSelected ;
110- if ( isCurrentOptionSelected ) {
111- return [ filteredCurrentUserOption , ...optionsList . selectedOptions , ...optionsList . personalDetails ] ;
112- }
113- return [ ...optionsList . selectedOptions , filteredCurrentUserOption , ...optionsList . personalDetails ] ;
114- } , [ filteredCurrentUserOption , optionsList . selectedOptions , optionsList . personalDetails ] ) ;
113+ const combinedOptionsWithKeyForList = combinedOptions . map ( ( option ) => ( {
114+ ...option ,
115+ keyForList : option . keyForList ?? option . login ?? '' ,
116+ } ) ) ;
117+ return combinedOptionsWithKeyForList ;
118+ } , [ availableOptions . personalDetails , availableOptions . recentReports , selectedOptionsForDisplay , currentUserAccountID ] ) ;
115119
116120 const headerMessage = useMemo ( ( ) => {
117121 const noResultsFound = isEmpty ( listData ) ;
@@ -120,40 +124,47 @@ function UserSelectPopup({value, closeOverlay, onChange, isSearchable}: UserSele
120124
121125 const selectUser = useCallback (
122126 ( option : OptionData ) => {
123- const isSelected = selectedAccountIDs . has ( option . accountID . toString ( ) ) ;
124-
125- setSelectedAccountIDs ( ( prev ) => ( isSelected ? new Set ( [ ...prev ] . filter ( ( id ) => id !== option . accountID . toString ( ) ) ) : new Set ( [ ...prev , option . accountID . toString ( ) ] ) ) ) ;
127+ toggleSelection ( option ) ;
126128 selectionListRef ?. current ?. scrollToIndex ( 0 ) ;
127129 } ,
128- [ selectedAccountIDs ] ,
130+ [ toggleSelection ] ,
129131 ) ;
130132
131133 const applyChanges = useCallback ( ( ) => {
132- const accountIDs = Array . from ( selectedAccountIDs ) ;
134+ const accountIDs = selectedOptions . flatMap ( ( option ) => ( option . accountID ? [ option . accountID . toString ( ) ] : [ ] ) ) ;
133135 closeOverlay ( ) ;
134136 onChange ( accountIDs ) ;
135- } , [ closeOverlay , onChange , selectedAccountIDs ] ) ;
137+ } , [ closeOverlay , onChange , selectedOptions ] ) ;
136138
137139 const resetChanges = useCallback ( ( ) => {
138140 onChange ( [ ] ) ;
139141 closeOverlay ( ) ;
140142 } , [ closeOverlay , onChange ] ) ;
141143
142144 const isLoadingNewOptions = ! ! isSearchingForReports ;
143- const shouldShowSearchInput = isSearchable ?? transformedOptions . length >= CONST . STANDARD_LIST_ITEM_LIMIT ;
145+ const [ totalOptionsCount , setTotalOptionsCount ] = useState ( ( ) => selectedOptionsForDisplay . length + availableOptions . personalDetails . length + availableOptions . recentReports . length ) ;
146+
147+ useEffect ( ( ) => {
148+ if ( debouncedSearchTerm ) {
149+ return ;
150+ }
151+ setTotalOptionsCount ( selectedOptionsForDisplay . length + availableOptions . personalDetails . length + availableOptions . recentReports . length ) ;
152+ } , [ debouncedSearchTerm , selectedOptionsForDisplay . length , availableOptions . personalDetails . length , availableOptions . recentReports . length ] ) ;
153+
154+ const shouldShowSearchInput = isSearchable ?? totalOptionsCount >= CONST . STANDARD_LIST_ITEM_LIMIT ;
144155
145156 const textInputOptions = useMemo (
146157 ( ) =>
147158 shouldShowSearchInput
148159 ? {
149160 value : searchTerm ,
150- label : translate ( 'common.search ' ) ,
161+ label : translate ( 'selectionList.searchForSomeone ' ) ,
151162 onChangeText : setSearchTerm ,
152163 headerMessage,
153164 disableAutoFocus : ! shouldFocusInputOnScreenFocus ,
154165 }
155166 : undefined ,
156- [ searchTerm , translate , headerMessage , shouldFocusInputOnScreenFocus , shouldShowSearchInput ] ,
167+ [ shouldShowSearchInput , searchTerm , translate , setSearchTerm , headerMessage , shouldFocusInputOnScreenFocus ] ,
157168 ) ;
158169
159170 return (
@@ -167,6 +178,8 @@ function UserSelectPopup({value, closeOverlay, onChange, isSearchable}: UserSele
167178 style = { { containerStyle : [ ! shouldUseNarrowLayout && styles . pt4 ] , listStyle : styles . pb2 } }
168179 onSelectRow = { selectUser }
169180 isLoadingNewOptions = { isLoadingNewOptions }
181+ shouldShowLoadingPlaceholder = { ! areOptionsInitialized }
182+ onEndReached = { onListEndReached }
170183 />
171184
172185 < View style = { [ styles . flexRow , styles . gap2 , styles . mh5 , ! shouldUseNarrowLayout && styles . mb4 ] } >
0 commit comments