Repository: Expensify/App (language: typescript)

Issue:
Add the Recents and Contacts headers to the single-user participants page used in the Request Money and New Chat flows 
If you haven’t already, check out our [contributing guidelines](https://github.com/Expensify/ReactNativeChat/blob/main/CONTRIBUTING.md) for onboarding and email contributors@expensify.com to request to join our Slack channel!
___

**How to enable the `Request Money` or `Spilt Bills` flow**
As the feature is currently behind a beta, you must perform the following change to reproduce the issue. Either navigate to Permissions.js,[ comment out this line ](https://github.com/Expensify/Expensify.cash/blob/e28b50e139c492442ea984c3d48e23be744ceb4e/src/libs/Permissions.js#L31)and return true -- or apply this [diff.txt](https://github.com/Expensify/Expensify/files/6427621/diff.txt) with ` git apply path/to/diff.txt`

## Expected Result:
- We should consistently show the `recents` and `contacts` headers on the participant selector pages across all four of these flows: `Request Money`, `Split Bill`, `New Chat` and `New Group`.

## Actual Result:
- We don't display the headers on the single-user picker on the participants page within the  `Request Money` or `New Chat` flows today, but we do for the multi-user picker in the `Split Bill` & `New Group` flows.

## Action Performed:
1. Click the `+` icon on the home screen to access the CreateMenu
2. Select `Request Money`
3. Enter an amount and click `next` to access the single-user picker on the IOUParticipants page 

OR

1. Click the `+` icon on the home screen to access the CreateMenu
2. Select `New Chat` to access the single-user picker on the Participants page

## Workaround:
Users can still search for users, this is an optimization in convenience and recent activity. 

## Platform:
- All platforms 

**Version Number:** `v1.0.37-0`

**Notes/Photos/Videos:** Here's an example of how `Request Money` should look:
![image](https://user-images.githubusercontent.com/16232057/117209193-f12c5180-aded-11eb-9ee6-f37806664654.png)

**Expensify/Expensify Issue URL:** https://github.com/Expensify/Expensify/issues/162993

[View all open jobs on Upwork](https://www.upwork.com/ab/jobs/search/?q=Expensify%20React%20Native&sort=recency&user_location_match=2)



Discussion hints:
Triggered auto assignment to @mallenexpensify (`External`), see https://stackoverflow.com/c/expensify/questions/8582 for more details.
Triggered auto assignment to @chiragsalian (`Engineering`), see https://stackoverflow.com/c/expensify/questions/4319 for more details.
Sorry about that. Disregard @chiragsalian and @mallenexpensify 
Posting to Upwork now. 
Thanks for checking us out on Upwork!

If you are interested in working on this issue, please apply to [this Upwork posting](https://www.upwork.com/jobs/~015652383fbbeae713), AND then post directly on this issue with a proposal with the technical explanation of the changes you will make. Somebody from Expensify will review your proposal, and if your proposal is accepted, you will be hired in Upwork for the job.

Thank you!

Pull request: Add the Recents and Contacts headers to the single-user participants page used in the Request Money and New Chat flows
### Details
We are now consistently show the `recents` and `contacts` headers on the participant selector pages across all four of these flows: `Request Money`, `Split Bill`, `New Chat` and New` Group`.

### Fixed Issues
Fixed #2760 

### Tests

1. Click the `+` icon on the home screen to access the CreateMenu
2. Select `Request Money
`3. Enter an amount and click `next` to access the single-user picker on the IOUParticipants page

OR

1. Click the `+` icon on the home screen to access the CreateMenu
2. Select `New Chat` to access the single-user picker on the Participants page

### QA Steps
See above

### Tested On

- [x] Web
- [x] Mobile Web
- [x] Desktop
- [x] iOS
- [x] Android

### Screenshots

#### Web
![Screen Shot 2021-05-13 at 4 59 02 PM](https://user-images.githubusercontent.com/11542415/118122669-9b1e6600-b40c-11eb-9678-03d678c0808e.png)

#### Mobile Web
![Screen Shot 2021-05-13 at 5 11 34 PM](https://user-images.githubusercontent.com/11542415/118123874-47ad1780-b40e-11eb-96af-1cbf577386dc.png)


#### Desktop
![Screen Shot 2021-05-13 at 5 11 49 PM](https://user-images.githubusercontent.com/11542415/118123908-50055280-b40e-11eb-90d5-01dfd88b4e09.png)


#### iOS
![Screen Shot 2021-05-13 at 5 05 00 PM](https://user-images.githubusercontent.com/11542415/118123214-5e9f3a00-b40d-11eb-9199-56a2f53cffc4.png) 

#### Android
![Screen Shot 2021-05-13 at 5 10 30 PM](https://user-images.githubusercontent.com/11542415/118123748-23513b00-b40e-11eb-8c8f-349bda947d6c.png)



Test command: NODE_OPTIONS='--experimental-vm-modules --max-old-space-size=4096' npx --no-install jest tests/unit/OptionsListUtilsTest.js --json --outputFile=/tmp/test-results.json --passWithNoTests 2>&1

Fix patch:
diff --git a/src/components/OptionsList.js b/src/components/OptionsList.js
index 5629831e7f49..96c06425fbb9 100644
--- a/src/components/OptionsList.js
+++ b/src/components/OptionsList.js
@@ -188,7 +188,7 @@ class OptionsList extends Component {
         if (title && shouldShow && !this.props.hideSectionHeaders) {
             return (
                 <View>
-                    <Text style={[styles.p5, styles.textMicroBold, styles.colorHeading]}>
+                    <Text style={[styles.p5, styles.textMicroBold, styles.colorHeading, styles.textUppercase]}>
                         {title}
                     </Text>
                 </View>
diff --git a/src/languages/en.js b/src/languages/en.js
index ea9b5061e42f..6b1eff6d2840 100755
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -27,6 +27,8 @@ export default {
         email: 'Email',
         and: 'and',
         details: 'Details',
+        contacts: 'Contacts',
+        recents: 'Recents',
     },
     attachmentPicker: {
         cameraPermissionRequired: 'Camera Permission Required',
@@ -93,8 +95,6 @@ export default {
         newGroup: 'New Group',
     },
     iou: {
-        contacts: 'CONTACTS',
-        recents: 'RECENTS',
         amount: 'Amount',
         participants: 'Participants',
         confirm: 'Confirm',
diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js
index f53835f9a116..c02ceb802278 100644
--- a/src/libs/OptionsListUtils.js
+++ b/src/libs/OptionsListUtils.js
@@ -364,6 +364,8 @@ function getNewChatOptions(
     return getOptions(reports, personalDetails, {}, 0, {
         searchValue,
         includePersonalDetails: true,
+        includeRecentReports: true,
+        maxRecentReportsToShow: 5,
         excludeConcierge,
     });
 }
diff --git a/src/pages/NewChatPage.js b/src/pages/NewChatPage.js
index b87a755f288f..6922f9d81cce 100755
--- a/src/pages/NewChatPage.js
+++ b/src/pages/NewChatPage.js
@@ -2,6 +2,7 @@ import React, {Component} from 'react';
 import {View} from 'react-native';
 import PropTypes from 'prop-types';
 import {withOnyx} from 'react-native-onyx';
+import _ from 'underscore';
 import OptionsSelector from '../components/OptionsSelector';
 import {getNewChatOptions, getHeaderMessage} from '../libs/OptionsListUtils';
 import ONYXKEYS from '../ONYXKEYS';
@@ -54,6 +55,7 @@ class NewChatPage extends Component {
         this.createNewChat = this.createNewChat.bind(this);
 
         const {
+            recentReports,
             personalDetails,
             userToInvite,
         } = getNewChatOptions(
@@ -64,6 +66,7 @@ class NewChatPage extends Component {
 
         this.state = {
             searchValue: '',
+            recentReports,
             personalDetails,
             userToInvite,
         };
@@ -78,9 +81,16 @@ class NewChatPage extends Component {
         const sections = [];
 
         sections.push({
-            title: this.props.translate('iou.contacts'),
+            title: this.props.translate('common.recents'),
+            data: this.state.recentReports,
+            shouldShow: !_.isEmpty(this.state.recentReports),
+            indexOffset: sections.reduce((prev, {data}) => prev + data.length, 0),
+        });
+
+        sections.push({
+            title: this.props.translate('common.contacts'),
             data: this.state.personalDetails,
-            shouldShow: this.state.personalDetails.length > 0,
+            shouldShow: !_.isEmpty(this.state.personalDetails),
             indexOffset: sections.reduce((prev, {data}) => prev + data.length, 0),
         });
 
@@ -128,6 +138,7 @@ class NewChatPage extends Component {
                         onSelectRow={this.createNewChat}
                         onChangeText={(searchValue = '') => {
                             const {
+                                recentReports,
                                 personalDetails,
                                 userToInvite,
                             } = getNewChatOptions(
@@ -137,12 +148,12 @@ class NewChatPage extends Component {
                             );
                             this.setState({
                                 searchValue,
+                                recentReports,
                                 userToInvite,
                                 personalDetails,
                             });
                         }}
                         headerMessage={headerMessage}
-                        hideSectionHeaders
                         disableArrowKeysActions
                         hideAdditionalOptionStates
                         forceTextUnreadStyle
diff --git a/src/pages/NewGroupPage.js b/src/pages/NewGroupPage.js
index 3a5bd94ec924..525f286863a2 100755
--- a/src/pages/NewGroupPage.js
+++ b/src/pages/NewGroupPage.js
@@ -97,16 +97,16 @@ class NewGroupPage extends Component {
         }
 
         sections.push({
-            title: this.props.translate('iou.recents'),
+            title: this.props.translate('common.recents'),
             data: this.state.recentReports,
-            shouldShow: this.state.recentReports.length > 0,
+            shouldShow: !_.isEmpty(this.state.recentReports),
             indexOffset: sections.reduce((prev, {data}) => prev + data.length, 0),
         });
 
         sections.push({
-            title: this.props.translate('iou.contacts'),
+            title: this.props.translate('common.contacts'),
             data: this.state.personalDetails,
-            shouldShow: this.state.personalDetails.length > 0,
+            shouldShow: !_.isEmpty(this.state.personalDetails),
             indexOffset: sections.reduce((prev, {data}) => prev + data.length, 0),
         });
 
diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js
index dff19ddea251..9d8a95e326e2 100755
--- a/src/pages/SearchPage.js
+++ b/src/pages/SearchPage.js
@@ -90,7 +90,7 @@ class SearchPage extends Component {
      */
     getSections() {
         const sections = [{
-            title: this.props.translate('iou.recents'),
+            title: this.props.translate('common.recents'),
             data: this.state.recentReports.concat(this.state.personalDetails),
             shouldShow: true,
             indexOffset: 0,
diff --git a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsRequest.js b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsRequest.js
index 56d1769a5c4a..19bdb905ccdf 100755
--- a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsRequest.js
+++ b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsRequest.js
@@ -1,6 +1,7 @@
 import React, {Component} from 'react';
 import PropTypes from 'prop-types';
 import {withOnyx} from 'react-native-onyx';
+import _ from 'underscore';
 import {getNewChatOptions} from '../../../../libs/OptionsListUtils';
 import OptionsSelector from '../../../../components/OptionsSelector';
 import ONYXKEYS from '../../../../ONYXKEYS';
@@ -44,7 +45,11 @@ class IOUParticipantsRequest extends Component {
 
         this.addSingleParticipant = this.addSingleParticipant.bind(this);
 
-        const {personalDetails, userToInvite} = getNewChatOptions(
+        const {
+            recentReports,
+            personalDetails,
+            userToInvite,
+        } = getNewChatOptions(
             props.reports,
             props.personalDetails,
             '',
@@ -52,6 +57,7 @@ class IOUParticipantsRequest extends Component {
         );
 
         this.state = {
+            recentReports,
             personalDetails,
             userToInvite,
             searchValue: '',
@@ -65,10 +71,18 @@ class IOUParticipantsRequest extends Component {
      */
     getSections() {
         const sections = [];
+
+        sections.push({
+            title: this.props.translate('common.recents'),
+            data: this.state.recentReports,
+            shouldShow: !_.isEmpty(this.state.recentReports),
+            indexOffset: sections.reduce((prev, {data}) => prev + data.length, 0),
+        });
+
         sections.push({
-            title: this.props.translate('iou.contacts'),
+            title: this.props.translate('common.contacts'),
             data: this.state.personalDetails,
-            shouldShow: this.state.personalDetails.length > 0,
+            shouldShow: !_.isEmpty(this.state.personalDetails),
             indexOffset: 0,
         });
 
@@ -102,7 +116,11 @@ class IOUParticipantsRequest extends Component {
                 value={this.state.searchValue}
                 onSelectRow={this.addSingleParticipant}
                 onChangeText={(searchValue = '') => {
-                    const {personalDetails, userToInvite} = getNewChatOptions(
+                    const {
+                        recentReports,
+                        personalDetails,
+                        userToInvite,
+                    } = getNewChatOptions(
                         this.props.reports,
                         this.props.personalDetails,
                         searchValue,
@@ -110,11 +128,11 @@ class IOUParticipantsRequest extends Component {
                     );
                     this.setState({
                         searchValue,
+                        recentReports,
                         userToInvite,
                         personalDetails,
                     });
                 }}
-                hideSectionHeaders
                 disableArrowKeysActions
                 hideAdditionalOptionStates
                 forceTextUnreadStyle
diff --git a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js
index f1c1b0f7baeb..df8b40695799 100755
--- a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js
+++ b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js
@@ -108,9 +108,9 @@ class IOUParticipantsSplit extends Component {
         }
 
         sections.push({
-            title: this.props.translate('iou.recents'),
+            title: this.props.translate('common.recents'),
             data: this.state.recentReports,
-            shouldShow: this.state.recentReports.length > 0,
+            shouldShow: !_.isEmpty(this.state.recentReports),
 
             // takes the sum off the length of all data
             // (this.state.selectedOptions) in previous sections
@@ -118,9 +118,9 @@ class IOUParticipantsSplit extends Component {
         });
 
         sections.push({
-            title: this.props.translate('iou.contacts'),
+            title: this.props.translate('common.contacts'),
             data: this.state.personalDetails,
-            shouldShow: this.state.personalDetails.length > 0,
+            shouldShow: !_.isEmpty(this.state.personalDetails),
 
             // takes the sum off the length of all data
             // (this.state.selectedOptions, this.state.recentReports) in previous sections
diff --git a/src/styles/styles.js b/src/styles/styles.js
index b6cbb169ef01..940649298cb4 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -89,6 +89,10 @@ const styles = {
         textDecorationLine: 'none',
     },
 
+    textUppercase: {
+        textTransform: 'uppercase',
+    },
+
     colorReversed: {
         color: themeColors.textReversed,
     },


Test patch:
diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js
index d24b3b5a2cdf..9ce34322cdfb 100644
--- a/tests/unit/OptionsListUtilsTest.js
+++ b/tests/unit/OptionsListUtilsTest.js
@@ -193,20 +193,25 @@ describe('OptionsListUtils', () => {
     });
 
     it('getNewChatOptions()', () => {
+        // maxRecentReportsToShow in src/libs/OptionsListUtils.js
+        const MAX_RECENT_REPORTS = 5;
+
         // When we call getNewChatOptions() with no search value
         let results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, '');
 
-        // Then no reports should be returned, only personalDetails and all the personalDetails should be returned
-        // minus the currently logged in user
-        expect(results.recentReports.length).toBe(0);
-        expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS) - 1);
+        // We should expect maximimum of 5 recent reports to be returned
+        expect(results.recentReports.length).toBe(MAX_RECENT_REPORTS);
+
+        // We should expect all personalDetails to be returned,
+        // minus the currently logged in user and recent reports count
+        expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS) - 1 - MAX_RECENT_REPORTS);
 
         // Then the result which has an existing report should also have the reportID attached
         const personalDetailWithExistingReport = _.find(
             results.personalDetails,
-            personalDetail => personalDetail.login === 'reedrichards@expensify.com',
+            personalDetail => personalDetail.login === 'peterparker@expensify.com',
         );
-        expect(personalDetailWithExistingReport.reportID).toBe(3);
+        expect(personalDetailWithExistingReport.reportID).toBe(2);
 
         // When we provide a search value that does not match any personal details
         results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, 'magneto');
@@ -217,27 +222,30 @@ describe('OptionsListUtils', () => {
         // When we provide a search value that matches an email
         results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, 'peterparker@expensify.com');
 
-        // Then one option will be returned and it will be the correct option
-        expect(results.personalDetails.length).toBe(1);
-        expect(results.personalDetails[0].text).toBe('Spider-Man');
+        // Then one recentReports will be returned and it will be the correct option
+        // personalDetails should be empty array
+        expect(results.recentReports.length).toBe(1);
+        expect(results.recentReports[0].text).toBe('Spider-Man');
+        expect(results.personalDetails.length).toBe(0);
 
         // When we provide a search value that matches a partial display name or email
         results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, 'man');
 
         // Then several options will be returned and they will be each have the search string in their email or name
         // even though the currently logged in user matches they should not show.
-        expect(results.personalDetails.length).toBe(3);
-        expect(results.personalDetails[0].text).toBe('Spider-Man');
-        expect(results.personalDetails[1].text).toBe('Invisible Woman');
-        expect(results.personalDetails[2].login).toBe('natasharomanoff@expensify.com');
+        expect(results.personalDetails.length).toBe(1);
+        expect(results.recentReports.length).toBe(2);
+        expect(results.personalDetails[0].login).toBe('natasharomanoff@expensify.com');
+        expect(results.recentReports[0].text).toBe('Invisible Woman');
+        expect(results.recentReports[1].text).toBe('Spider-Man');
 
         // Test for Concierge's existence in chat options
         results = OptionsListUtils.getNewChatOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE);
 
-        // Concierge is included in the results by default and all the personalDetails should be returned
-        // minus the currently logged in user
-        expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 1);
-        expect(results.personalDetails).toEqual(
+        // Concierge is included in the results by default. We should expect all the personalDetails to show
+        // (minus the 5 that are already showing and the currently logged in user)
+        expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 1 - MAX_RECENT_REPORTS);
+        expect(results.recentReports).toEqual(
             expect.arrayContaining([
                 expect.objectContaining({login: 'concierge@expensify.com'}),
             ]),
@@ -247,7 +255,7 @@ describe('OptionsListUtils', () => {
         results = OptionsListUtils.getNewChatOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE, '', true);
 
         // All the personalDetails should be returned minus the currently logged in user and Concierge
-        expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 2);
+        expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 2 - MAX_RECENT_REPORTS);
         expect(results.personalDetails).not.toEqual(
             expect.arrayContaining([
                 expect.objectContaining({login: 'concierge@expensify.com'}),