From 06382dbe4bccbed3662028996669823a5ed9f1f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Thu, 22 Feb 2024 17:22:48 +0100 Subject: [PATCH 1/7] moved to use useBetas usePersonalDetails hooks --- src/pages/NewChatPage.tsx | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 72393e89ae1a..1f9c1b55b1a0 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -4,6 +4,7 @@ import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import OfflineIndicator from '@components/OfflineIndicator'; +import {useBetas, usePersonalDetails} from '@components/OnyxProvider'; import OptionsSelector from '@components/OptionsSelector'; import ScreenWrapper from '@components/ScreenWrapper'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; @@ -28,11 +29,6 @@ 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; @@ -46,7 +42,7 @@ type NewChatPageProps = NewChatPageWithOnyxProps & { const excludedGroupEmails = CONST.EXPENSIFY_EMAILS.filter((value) => value !== CONST.EMAIL.CONCIERGE); -function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingForReports, dismissedReferralBanners}: NewChatPageProps) { +function NewChatPage({isGroupChat, reports, isSearchingForReports, dismissedReferralBanners}: NewChatPageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const [searchTerm, setSearchTerm] = useState(''); @@ -57,6 +53,8 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF const {isOffline} = useNetwork(); const {isSmallScreenWidth} = useWindowDimensions(); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); + const betas = useBetas(); + const personalDetails = usePersonalDetails(); const maxParticipantsReached = selectedOptions.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached); @@ -293,12 +291,6 @@ export default withOnyx({ reports: { key: ONYXKEYS.COLLECTION.REPORT, }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - betas: { - key: ONYXKEYS.BETAS, - }, isSearchingForReports: { key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, initWithStoredValues: false, From 680ad61fb0c1755f9ab7d5c402ab0d128a7beb0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Mon, 26 Feb 2024 16:04:20 +0100 Subject: [PATCH 2/7] refactor --- .../OptionsSelector/BaseOptionsSelector.js | 7 +- src/components/ReferralProgramCTA.tsx | 7 +- src/hooks/useStyledSafeAreaInsets.ts | 37 ++ src/libs/DoInteractionTask/index.ts | 3 +- src/pages/NewChatPage.tsx | 385 +++++++++--------- src/pages/NewChatSelectorPage.js | 77 ++-- src/pages/SearchPage/SearchPageFooter.tsx | 10 +- ...yForRefactorRequestParticipantsSelector.js | 20 +- .../MoneyRequestParticipantsSelector.js | 20 +- 9 files changed, 283 insertions(+), 283 deletions(-) create mode 100644 src/hooks/useStyledSafeAreaInsets.ts diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index 690897d548ce..fc9df3f11492 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -649,9 +649,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 1f9c1b55b1a0..c79c54340e67 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -1,20 +1,25 @@ +import {useFocusEffect} from '@react-navigation/native'; +import _ from 'lodash'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import {View} 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 {useBetas, usePersonalDetails} from '@components/OnyxProvider'; -import OptionsSelector from '@components/OptionsSelector'; -import ScreenWrapper from '@components/ScreenWrapper'; -import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import {PressableWithFeedback} from '@components/Pressable'; +import ReferralProgramCTA from '@components/ReferralProgramCTA'; +import SelectCircle from '@components/SelectCircle'; +import SelectionList from '@components/SelectionList'; +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'; @@ -23,15 +28,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; - /** 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; }; @@ -42,40 +43,85 @@ type NewChatPageProps = NewChatPageWithOnyxProps & { const excludedGroupEmails = CONST.EXPENSIFY_EMAILS.filter((value) => value !== CONST.EMAIL.CONCIERGE); -function NewChatPage({isGroupChat, reports, isSearchingForReports, dismissedReferralBanners}: NewChatPageProps) { +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 betas = useBetas(); - const personalDetails = usePersonalDetails(); - 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); + searchTerm, + debouncedSearchTerm, + setSearchTerm, + selectedOptions, + setSelectedOptions, + recentReports, + personalDetails, + userToInvite, + isOptionsDataReady, + } = useOptions({ + reports, + isGroupChat, + }); const sections = useMemo((): OptionsListUtils.CategorySection[] => { const sectionsList: OptionsListUtils.CategorySection[] = []; 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) { @@ -84,73 +130,31 @@ function NewChatPage({isGroupChat, reports, isSearchingForReports, dismissedRefe 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); - }; + }, [debouncedSearchTerm, selectedOptions, recentReports, personalDetails, maxParticipantsReached, translate, userToInvite]); /** * Creates a new 1:1 chat with the option and the current user, @@ -163,131 +167,134 @@ function NewChatPage({isGroupChat, reports, isSearchingForReports, dismissedRefe Report.navigateToAndOpenReport([option.login]); }; - /** - * 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. - */ - 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( + (item: 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: OptionData) { + const isOptionInList = !!option.isSelected; - 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]); + let newSelectedOptions; - useEffect(() => { - const interactionTask = doInteractionTask(() => { - setDidScreenTransitionEnd(true); - }); + if (isOptionInList) { + newSelectedOptions = _.reject(selectedOptions, (selectedOption) => selectedOption.login === option.login); + } else { + newSelectedOptions = [...selectedOptions, {...option, isSelected: true, selected: true}]; + } - return () => { - if (!interactionTask) { - return; + setSelectedOptions(newSelectedOptions); } - interactionTask.cancel(); - }; - }, []); + 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(() => { - if (!didScreenTransitionEnd) { - return; - } - updateOptions(); - }, [didScreenTransitionEnd, updateOptions]); + return ( +