diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js
index 40fb1115ac36..2694e2b83f7f 100755
--- a/src/components/OptionsSelector/BaseOptionsSelector.js
+++ b/src/components/OptionsSelector/BaseOptionsSelector.js
@@ -655,9 +655,10 @@ class BaseOptionsSelector extends Component {
)}
{this.props.shouldShowReferralCTA && (
-
-
-
+
)}
{shouldShowFooter && (
diff --git a/src/components/ReferralProgramCTA.tsx b/src/components/ReferralProgramCTA.tsx
index 6db37ce1320a..c5a91b3fdceb 100644
--- a/src/components/ReferralProgramCTA.tsx
+++ b/src/components/ReferralProgramCTA.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import type {StyleProp, ViewStyle} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
@@ -25,9 +26,11 @@ type ReferralProgramCTAProps = ReferralProgramCTAOnyxProps & {
| typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT
| typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY
| typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND;
+
+ style?: StyleProp;
};
-function ReferralProgramCTA({referralContentType, dismissedReferralBanners}: ReferralProgramCTAProps) {
+function ReferralProgramCTA({referralContentType, style, dismissedReferralBanners}: ReferralProgramCTAProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const theme = useTheme();
@@ -45,7 +48,7 @@ function ReferralProgramCTA({referralContentType, dismissedReferralBanners}: Ref
onPress={() => {
Navigation.navigate(ROUTES.REFERRAL_DETAILS_MODAL.getRoute(referralContentType, Navigation.getActiveRouteWithoutParams()));
}}
- style={[styles.w100, styles.br2, styles.highlightBG, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, {gap: 10, padding: 10}, styles.pl5]}
+ style={[styles.w100, styles.br2, styles.highlightBG, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, {gap: 10, padding: 10}, styles.pl5, style]}
accessibilityLabel="referral"
role={CONST.ACCESSIBILITY_ROLE.BUTTON}
>
diff --git a/src/hooks/useStyledSafeAreaInsets.ts b/src/hooks/useStyledSafeAreaInsets.ts
new file mode 100644
index 000000000000..bfd9c32a46ae
--- /dev/null
+++ b/src/hooks/useStyledSafeAreaInsets.ts
@@ -0,0 +1,37 @@
+// eslint-disable-next-line no-restricted-imports
+import {useSafeAreaInsets} from 'react-native-safe-area-context';
+import useStyleUtils from './useStyleUtils';
+
+/**
+ * Custom hook to get the styled safe area insets.
+ * This hook utilizes the `SafeAreaInsetsContext` to retrieve the current safe area insets
+ * and applies styling adjustments using the `useStyleUtils` hook.
+ *
+ * @returns An object containing the styled safe area insets and additional styles.
+ * @returns .paddingTop The top padding adjusted for safe area.
+ * @returns .paddingBottom The bottom padding adjusted for safe area.
+ * @returns .insets The safe area insets object or undefined if not available.
+ * @returns .safeAreaPaddingBottomStyle An object containing the bottom padding style adjusted for safe area.
+ *
+ * @example
+ * // How to use this hook in a component
+ * function MyComponent() {
+ * const { paddingTop, paddingBottom, safeAreaPaddingBottomStyle } = useStyledSafeAreaInsets();
+ *
+ * // Use these values to style your component accordingly
+ * }
+ */
+function useStyledSafeAreaInsets() {
+ const StyleUtils = useStyleUtils();
+ const insets = useSafeAreaInsets();
+
+ const {paddingTop, paddingBottom} = StyleUtils.getSafeAreaPadding(insets ?? undefined);
+ return {
+ paddingTop,
+ paddingBottom,
+ insets: insets ?? undefined,
+ safeAreaPaddingBottomStyle: {paddingBottom},
+ };
+}
+
+export default useStyledSafeAreaInsets;
diff --git a/src/libs/DoInteractionTask/index.ts b/src/libs/DoInteractionTask/index.ts
index 0eadb8f7dbee..c53f08be88dc 100644
--- a/src/libs/DoInteractionTask/index.ts
+++ b/src/libs/DoInteractionTask/index.ts
@@ -1,7 +1,8 @@
+import DomUtils from '@libs/DomUtils';
import type DoInteractionTask from './types';
const doInteractionTask: DoInteractionTask = (callback) => {
- callback();
+ DomUtils.requestAnimationFrame(callback);
return null;
};
diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx
index 72393e89ae1a..c707f937eed5 100755
--- a/src/pages/NewChatPage.tsx
+++ b/src/pages/NewChatPage.tsx
@@ -1,19 +1,28 @@
+import {useFocusEffect} from '@react-navigation/native';
+import isEmpty from 'lodash/isEmpty';
+import reject from 'lodash/reject';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
-import {View} from 'react-native';
+import type {SectionListData} from 'react-native';
+import {InteractionManager, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
+import Button from '@components/Button';
import KeyboardAvoidingView from '@components/KeyboardAvoidingView';
import OfflineIndicator from '@components/OfflineIndicator';
-import OptionsSelector from '@components/OptionsSelector';
-import ScreenWrapper from '@components/ScreenWrapper';
-import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import {useBetas, usePersonalDetails} from '@components/OnyxProvider';
+import {PressableWithFeedback} from '@components/Pressable';
+import ReferralProgramCTA from '@components/ReferralProgramCTA';
+import SelectCircle from '@components/SelectCircle';
+import SelectionList from '@components/SelectionList';
+import type {ListItem, Section} from '@components/SelectionList/types';
+import UserListItem from '@components/SelectionList/UserListItem';
+import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
-import useSearchTermAndSearch from '@hooks/useSearchTermAndSearch';
+import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
-import doInteractionTask from '@libs/DoInteractionTask';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as ReportUtils from '@libs/ReportUtils';
import type {OptionData} from '@libs/ReportUtils';
@@ -22,20 +31,11 @@ import * as Report from '@userActions/Report';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type * as OnyxTypes from '@src/types/onyx';
-import type {DismissedReferralBanners} from '@src/types/onyx/Account';
type NewChatPageWithOnyxProps = {
/** All reports shared with the user */
reports: OnyxCollection;
- /** All of the personal details for everyone */
- personalDetails: OnyxEntry;
-
- betas: OnyxEntry;
-
- /** An object that holds data about which referral banners have been dismissed */
- dismissedReferralBanners: DismissedReferralBanners;
-
/** Whether we are searching for reports in the server */
isSearchingForReports: OnyxEntry;
};
@@ -46,119 +46,126 @@ type NewChatPageProps = NewChatPageWithOnyxProps & {
const excludedGroupEmails = CONST.EXPENSIFY_EMAILS.filter((value) => value !== CONST.EMAIL.CONCIERGE);
-function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingForReports, dismissedReferralBanners}: NewChatPageProps) {
+const EMPTY_ARRAY: Array>> = [];
+
+function useOptions({reports, isGroupChat}: Omit) {
+ const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState('');
+ const personalDetails = usePersonalDetails();
+ const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails);
+ const [selectedOptions, setSelectedOptions] = useState>([]);
+ const betas = useBetas();
+
+ const options = useMemo(() => {
+ const filteredOptions = OptionsListUtils.getFilteredOptions(
+ reports,
+ personalDetails,
+ betas,
+ debouncedSearchTerm,
+ selectedOptions,
+ isGroupChat ? excludedGroupEmails : [],
+ false,
+ true,
+ false,
+ {},
+ [],
+ false,
+ {},
+ [],
+ true,
+ false,
+ );
+ const maxParticipantsReached = selectedOptions.length === CONST.REPORT.MAXIMUM_PARTICIPANTS;
+
+ const headerMessage = OptionsListUtils.getHeaderMessage(
+ filteredOptions.personalDetails.length + filteredOptions.recentReports.length !== 0,
+ Boolean(filteredOptions.userToInvite),
+ debouncedSearchTerm.trim(),
+ maxParticipantsReached,
+ selectedOptions.some((participant) => participant?.searchText?.toLowerCase?.().includes(debouncedSearchTerm.trim().toLowerCase())),
+ );
+ return {...filteredOptions, headerMessage, maxParticipantsReached};
+ }, [betas, debouncedSearchTerm, isGroupChat, personalDetails, reports, selectedOptions]);
+
+ useEffect(() => {
+ if (!debouncedSearchTerm.length || options.maxParticipantsReached) {
+ return;
+ }
+
+ Report.searchInServer(debouncedSearchTerm);
+ }, [debouncedSearchTerm, options.maxParticipantsReached]);
+
+ return {...options, searchTerm, debouncedSearchTerm, setSearchTerm, isOptionsDataReady, selectedOptions, setSelectedOptions};
+}
+
+function NewChatPage({isGroupChat, reports, isSearchingForReports}: NewChatPageProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
- const [searchTerm, setSearchTerm] = useState('');
- const [filteredRecentReports, setFilteredRecentReports] = useState([]);
- const [filteredPersonalDetails, setFilteredPersonalDetails] = useState([]);
- const [filteredUserToInvite, setFilteredUserToInvite] = useState();
- const [selectedOptions, setSelectedOptions] = useState([]);
+ const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false);
const {isOffline} = useNetwork();
const {isSmallScreenWidth} = useWindowDimensions();
- const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false);
-
- const maxParticipantsReached = selectedOptions.length === CONST.REPORT.MAXIMUM_PARTICIPANTS;
- const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached);
- const headerMessage = OptionsListUtils.getHeaderMessage(
- filteredPersonalDetails.length + filteredRecentReports.length !== 0,
- Boolean(filteredUserToInvite),
- searchTerm.trim(),
+ const {
+ headerMessage,
maxParticipantsReached,
- selectedOptions.some((participant) => participant?.searchText?.toLowerCase().includes(searchTerm.trim().toLowerCase())),
- );
-
- const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails);
-
- const sections = useMemo((): OptionsListUtils.CategorySection[] => {
- const sectionsList: OptionsListUtils.CategorySection[] = [];
+ searchTerm,
+ debouncedSearchTerm,
+ setSearchTerm,
+ selectedOptions,
+ setSelectedOptions,
+ recentReports,
+ personalDetails,
+ userToInvite,
+ isOptionsDataReady,
+ } = useOptions({
+ reports,
+ isGroupChat,
+ });
+
+ const sections = useMemo(() => {
+ const sectionsList = [];
let indexOffset = 0;
- const formatResults = OptionsListUtils.formatSectionsFromSearchTerm(searchTerm, selectedOptions, filteredRecentReports, filteredPersonalDetails, maxParticipantsReached, indexOffset);
+ const formatResults = OptionsListUtils.formatSectionsFromSearchTerm(debouncedSearchTerm, selectedOptions, recentReports, personalDetails, true, indexOffset);
sectionsList.push(formatResults.section);
-
indexOffset = formatResults.newIndexOffset;
if (maxParticipantsReached) {
- return sectionsList;
+ return sectionsList as unknown as Array>>;
}
sectionsList.push({
title: translate('common.recents'),
- data: filteredRecentReports,
- shouldShow: filteredRecentReports.length > 0,
+ data: recentReports,
+ shouldShow: !isEmpty(recentReports),
indexOffset,
});
- indexOffset += filteredRecentReports.length;
+ indexOffset += recentReports.length;
sectionsList.push({
title: translate('common.contacts'),
- data: filteredPersonalDetails,
- shouldShow: filteredPersonalDetails.length > 0,
+ data: personalDetails,
+ shouldShow: !isEmpty(personalDetails),
indexOffset,
});
- indexOffset += filteredPersonalDetails.length;
+ indexOffset += personalDetails.length;
- if (filteredUserToInvite) {
+ if (userToInvite) {
sectionsList.push({
title: undefined,
- data: [filteredUserToInvite],
+ data: [userToInvite],
shouldShow: true,
indexOffset,
});
}
- return sectionsList;
- }, [translate, filteredPersonalDetails, filteredRecentReports, filteredUserToInvite, maxParticipantsReached, selectedOptions, searchTerm]);
-
- /**
- * Removes a selected option from list if already selected. If not already selected add this option to the list.
- */
- const toggleOption = (option: OptionData) => {
- const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option.login);
-
- let newSelectedOptions;
-
- if (isOptionInList) {
- newSelectedOptions = selectedOptions.filter((selectedOption) => selectedOption.login !== option.login);
- } else {
- newSelectedOptions = [...selectedOptions, option];
- }
-
- const {
- recentReports,
- personalDetails: newChatPersonalDetails,
- userToInvite,
- } = OptionsListUtils.getFilteredOptions(
- reports,
- personalDetails,
- betas ?? [],
- searchTerm,
- newSelectedOptions,
- isGroupChat ? excludedGroupEmails : [],
- false,
- true,
- false,
- {},
- [],
- false,
- {},
- [],
- true,
- );
-
- setSelectedOptions(newSelectedOptions);
- setFilteredRecentReports(recentReports);
- setFilteredPersonalDetails(newChatPersonalDetails);
- setFilteredUserToInvite(userToInvite);
- };
+ return sectionsList as unknown as Array>>;
+ }, [debouncedSearchTerm, selectedOptions, recentReports, personalDetails, maxParticipantsReached, translate, userToInvite]);
/**
* 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 createChat = (option: OptionData) => {
+ const createChat = (option: ListItem) => {
if (!option.login) {
return;
}
@@ -166,139 +173,141 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF
};
/**
- * Creates a new group chat with all the selected options and the current user,
- * or navigates to the existing chat if one with those participants already exists.
+ * This hook is used to set the state of didScreenTransitionEnd to true after the screen has transitioned.
+ * This is used to prevent the screen from rendering sections until transition has ended.
*/
- const createGroup = () => {
- const logins = selectedOptions.map((option) => option.login).filter((login): login is string => typeof login === 'string');
+ useFocusEffect(
+ React.useCallback(() => {
+ const task = InteractionManager.runAfterInteractions(() => {
+ setDidScreenTransitionEnd(true);
+ });
- if (logins.length < 1) {
- return;
- }
+ return () => task.cancel();
+ }, []),
+ );
- Report.navigateToAndOpenReport(logins);
- };
+ const itemRightSideComponent = useCallback(
+ (listItem: ListItem) => {
+ const item = listItem as ListItem & OptionData;
+ /**
+ * Removes a selected option from list if already selected. If not already selected add this option to the list.
+ * @param option
+ */
+ function toggleOption(option: ListItem & OptionData) {
+ const isOptionInList = !!option.isSelected;
+
+ let newSelectedOptions: Array;
+
+ if (isOptionInList) {
+ newSelectedOptions = reject(selectedOptions, (selectedOption) => selectedOption.login === option.login);
+ } else {
+ newSelectedOptions = [...selectedOptions, {...option, isSelected: true, selected: true}];
+ }
+
+ setSelectedOptions(newSelectedOptions);
+ }
- const updateOptions = useCallback(() => {
- const {
- recentReports,
- personalDetails: newChatPersonalDetails,
- userToInvite,
- } = OptionsListUtils.getFilteredOptions(
- reports,
- personalDetails,
- betas ?? [],
- searchTerm,
- selectedOptions,
- isGroupChat ? excludedGroupEmails : [],
- false,
- true,
- false,
- {},
- [],
- false,
- {},
- [],
- true,
- );
- setFilteredRecentReports(recentReports);
- setFilteredPersonalDetails(newChatPersonalDetails);
- setFilteredUserToInvite(userToInvite);
- // props.betas is not added as dependency since it doesn't change during the component lifecycle
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [reports, personalDetails, searchTerm]);
+ if (item.isSelected) {
+ return (
+ toggleOption(item)}
+ disabled={item.isDisabled}
+ role={CONST.ACCESSIBILITY_ROLE.CHECKBOX}
+ accessibilityLabel={CONST.ACCESSIBILITY_ROLE.CHECKBOX}
+ style={[styles.flexRow, styles.alignItemsCenter, styles.ml3]}
+ >
+
+
+ );
+ }
- useEffect(() => {
- const interactionTask = doInteractionTask(() => {
- setDidScreenTransitionEnd(true);
- });
+ return (
+