diff --git a/src/components/OnyxProvider.tsx b/src/components/OnyxProvider.tsx index 124f3558df90..d14aec90fa10 100644 --- a/src/components/OnyxProvider.tsx +++ b/src/components/OnyxProvider.tsx @@ -10,11 +10,12 @@ const [withPersonalDetails, PersonalDetailsProvider, , usePersonalDetails] = cre const [withCurrentDate, CurrentDateProvider] = createOnyxContext(ONYXKEYS.CURRENT_DATE); const [withReportActionsDrafts, ReportActionsDraftsProvider] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS); const [withBlockedFromConcierge, BlockedFromConciergeProvider] = createOnyxContext(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE); -const [withBetas, BetasProvider, BetasContext] = createOnyxContext(ONYXKEYS.BETAS); +const [withBetas, BetasProvider, BetasContext, useBetas] = createOnyxContext(ONYXKEYS.BETAS); const [withReportCommentDrafts, ReportCommentDraftsProvider] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT); const [withPreferredTheme, PreferredThemeProvider, PreferredThemeContext] = createOnyxContext(ONYXKEYS.PREFERRED_THEME); const [withFrequentlyUsedEmojis, FrequentlyUsedEmojisProvider, , useFrequentlyUsedEmojis] = createOnyxContext(ONYXKEYS.FREQUENTLY_USED_EMOJIS); const [withPreferredEmojiSkinTone, PreferredEmojiSkinToneProvider, PreferredEmojiSkinToneContext] = createOnyxContext(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE); +const [, SessionProvider, , useSession] = createOnyxContext(ONYXKEYS.SESSION); type OnyxProviderProps = { /** Rendered child component */ @@ -35,6 +36,7 @@ function OnyxProvider(props: OnyxProviderProps) { PreferredThemeProvider, FrequentlyUsedEmojisProvider, PreferredEmojiSkinToneProvider, + SessionProvider, ]} > {props.children} @@ -59,8 +61,10 @@ export { withReportCommentDrafts, withPreferredTheme, PreferredThemeContext, + useBetas, withFrequentlyUsedEmojis, useFrequentlyUsedEmojis, withPreferredEmojiSkinTone, PreferredEmojiSkinToneContext, + useSession, }; diff --git a/src/components/withCurrentUserPersonalDetails.tsx b/src/components/withCurrentUserPersonalDetails.tsx index 9406c8634c1b..313bcad74f35 100644 --- a/src/components/withCurrentUserPersonalDetails.tsx +++ b/src/components/withCurrentUserPersonalDetails.tsx @@ -1,26 +1,17 @@ import type {ComponentType, ForwardedRef, RefAttributes} from 'react'; -import React, {useMemo} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import React from 'react'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import getComponentDisplayName from '@libs/getComponentDisplayName'; import personalDetailsPropType from '@pages/personalDetailsPropType'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetails, Session} from '@src/types/onyx'; -import {usePersonalDetails} from './OnyxProvider'; +import type {PersonalDetails} from '@src/types/onyx'; type CurrentUserPersonalDetails = PersonalDetails | Record; -type OnyxProps = { - /** Session of the current user */ - session: OnyxEntry; -}; - type HOCProps = { currentUserPersonalDetails: CurrentUserPersonalDetails; }; -type WithCurrentUserPersonalDetailsProps = OnyxProps & HOCProps; +type WithCurrentUserPersonalDetailsProps = HOCProps; // TODO: remove when all components that use it will be migrated to TS const withCurrentUserPersonalDetailsPropTypes = { @@ -33,15 +24,9 @@ const withCurrentUserPersonalDetailsDefaultProps: HOCProps = { export default function ( WrappedComponent: ComponentType>, -): ComponentType & RefAttributes, keyof OnyxProps>> { +): ComponentType & RefAttributes> { function WithCurrentUserPersonalDetails(props: Omit, ref: ForwardedRef) { - const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; - const accountID = props.session?.accountID ?? 0; - const accountPersonalDetails = personalDetails?.[accountID]; - const currentUserPersonalDetails: CurrentUserPersonalDetails = useMemo( - () => (accountPersonalDetails ? {...accountPersonalDetails, accountID} : {}) as CurrentUserPersonalDetails, - [accountPersonalDetails, accountID], - ); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); return ( & RefAttributes, OnyxProps>({ - session: { - key: ONYXKEYS.SESSION, - }, - })(withCurrentUserPersonalDetails); + return React.forwardRef(WithCurrentUserPersonalDetails); } export {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps}; diff --git a/src/hooks/useCurrentUserPersonalDetails.ts b/src/hooks/useCurrentUserPersonalDetails.ts new file mode 100644 index 000000000000..da3c2b18bd83 --- /dev/null +++ b/src/hooks/useCurrentUserPersonalDetails.ts @@ -0,0 +1,21 @@ +import {useMemo} from 'react'; +import {usePersonalDetails, useSession} from '@components/OnyxProvider'; +import CONST from '@src/CONST'; +import type {PersonalDetails} from '@src/types/onyx'; + +type CurrentUserPersonalDetails = PersonalDetails | Record; + +function useCurrentUserPersonalDetails() { + const session = useSession(); + const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; + const accountID = session?.accountID ?? 0; + const accountPersonalDetails = personalDetails?.[accountID]; + const currentUserPersonalDetails: CurrentUserPersonalDetails = useMemo( + () => (accountPersonalDetails ? {...accountPersonalDetails, accountID} : {}) as CurrentUserPersonalDetails, + [accountPersonalDetails, accountID], + ); + + return currentUserPersonalDetails; +} + +export default useCurrentUserPersonalDetails; diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.js b/src/pages/tasks/TaskAssigneeSelectorModal.js index f3627a9bd04d..0619fe1abf15 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.js +++ b/src/pages/tasks/TaskAssigneeSelectorModal.js @@ -1,18 +1,19 @@ /* eslint-disable es/no-optional-chaining */ +import {useRoute} from '@react-navigation/native'; import lodashGet from 'lodash/get'; +import lodashPick from 'lodash/pick'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {usePersonalDetails} from '@components/OnyxProvider'; -import OptionsSelector from '@components/OptionsSelector'; +import {useBetas, usePersonalDetails, useSession} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; -import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import SelectionList from '@components/SelectionList'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useDebouncedState from '@hooks/useDebouncedState'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; @@ -25,29 +26,9 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; const propTypes = { - /** Beta features list */ - betas: PropTypes.arrayOf(PropTypes.string), - /** All reports shared with the user */ reports: PropTypes.objectOf(reportPropTypes), - /** URL Route params */ - route: PropTypes.shape({ - /** Params from the URL path */ - params: PropTypes.shape({ - /** reportID passed via route: /r/:reportID/title */ - reportID: PropTypes.string, - }), - }), - - // /** The report currently being looked at */ - // report: reportPropTypes.isRequired, - - /** Current user session */ - session: PropTypes.shape({ - email: PropTypes.string.isRequired, - }), - /** Grab the Share destination of the Task */ task: PropTypes.shape({ /** Share destination of the Task */ @@ -62,38 +43,26 @@ const propTypes = { /** The role of current user */ role: PropTypes.string, }), - - ...withLocalizePropTypes, }; const defaultProps = { - betas: [], reports: {}, - session: {}, - route: {}, task: {}, rootParentReportPolicy: {}, }; -function TaskAssigneeSelectorModal(props) { - const styles = useThemeStyles(); - const [searchValue, setSearchValue] = useState(''); - const [headerMessage, setHeaderMessage] = useState(''); - const [filteredRecentReports, setFilteredRecentReports] = useState([]); - const [filteredPersonalDetails, setFilteredPersonalDetails] = useState([]); - const [filteredUserToInvite, setFilteredUserToInvite] = useState(null); - const [filteredCurrentUserOption, setFilteredCurrentUserOption] = useState(null); - const [isLoading, setIsLoading] = React.useState(true); +function useOptions({reports}) { const allPersonalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; + const betas = useBetas(); + const [isLoading, setIsLoading] = useState(true); + const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); - const {inputCallbackRef} = useAutoFocusInput(); - - const updateOptions = useCallback(() => { + const options = useMemo(() => { const {recentReports, personalDetails, userToInvite, currentUserOption} = OptionsListUtils.getFilteredOptions( - props.reports, + reports, allPersonalDetails, - props.betas, - searchValue.trim(), + betas, + debouncedSearchValue.trim(), [], CONST.EXPENSIFY_EMAILS, false, @@ -107,50 +76,56 @@ function TaskAssigneeSelectorModal(props) { true, ); - setHeaderMessage(OptionsListUtils.getHeaderMessage(recentReports?.length + personalDetails?.length !== 0 || currentUserOption, Boolean(userToInvite), searchValue)); + const headerMessage = OptionsListUtils.getHeaderMessage(recentReports?.length + personalDetails?.length !== 0 || currentUserOption, Boolean(userToInvite), debouncedSearchValue); - setFilteredUserToInvite(userToInvite); - setFilteredRecentReports(recentReports); - setFilteredPersonalDetails(personalDetails); - setFilteredCurrentUserOption(currentUserOption); if (isLoading) { setIsLoading(false); } - }, [props, searchValue, allPersonalDetails, isLoading]); - useEffect(() => { - const debouncedSearch = _.debounce(updateOptions, 200); - debouncedSearch(); - return () => { - debouncedSearch.cancel(); + return { + userToInvite, + recentReports, + personalDetails, + currentUserOption, + headerMessage, }; - }, [updateOptions]); + }, [debouncedSearchValue, allPersonalDetails, isLoading, betas, reports]); + + return {...options, isLoading, searchValue, debouncedSearchValue, setSearchValue}; +} + +function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) { + const styles = useThemeStyles(); + const route = useRoute(); + const {translate} = useLocalize(); + const session = useSession(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const {userToInvite, recentReports, personalDetails, currentUserOption, isLoading, searchValue, setSearchValue, headerMessage} = useOptions({reports, task}); const onChangeText = (newSearchTerm = '') => { setSearchValue(newSearchTerm); }; const report = useMemo(() => { - if (!props.route.params || !props.route.params.reportID) { + if (!route.params || !route.params.reportID) { return null; } - return props.reports[`${ONYXKEYS.COLLECTION.REPORT}${props.route.params.reportID}`]; - }, [props.reports, props.route.params]); - - if (report && !ReportUtils.isTaskReport(report)) { - Navigation.isNavigationReady().then(() => { - Navigation.dismissModal(report.reportID); - }); - } + if (report && !ReportUtils.isTaskReport(report)) { + Navigation.isNavigationReady().then(() => { + Navigation.dismissModal(report.reportID); + }); + } + return reports[`${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`]; + }, [reports, route]); const sections = useMemo(() => { const sectionsList = []; let indexOffset = 0; - if (filteredCurrentUserOption) { + if (currentUserOption) { sectionsList.push({ - title: props.translate('newTaskPage.assignMe'), - data: [filteredCurrentUserOption], + title: translate('newTaskPage.assignMe'), + data: [currentUserOption], shouldShow: true, indexOffset, }); @@ -158,31 +133,31 @@ function TaskAssigneeSelectorModal(props) { } sectionsList.push({ - title: props.translate('common.recents'), - data: filteredRecentReports, - shouldShow: filteredRecentReports?.length > 0, + title: translate('common.recents'), + data: recentReports, + shouldShow: recentReports?.length > 0, indexOffset, }); - indexOffset += filteredRecentReports?.length; + indexOffset += recentReports?.length; sectionsList.push({ - title: props.translate('common.contacts'), - data: filteredPersonalDetails, - shouldShow: filteredPersonalDetails?.length > 0, + title: translate('common.contacts'), + data: personalDetails, + shouldShow: personalDetails?.length > 0, indexOffset, }); - indexOffset += filteredPersonalDetails?.length; + indexOffset += personalDetails?.length; - if (filteredUserToInvite) { + if (userToInvite) { sectionsList.push({ - data: [filteredUserToInvite], + data: [userToInvite], shouldShow: true, indexOffset, }); } return sectionsList; - }, [filteredCurrentUserOption, filteredPersonalDetails, filteredRecentReports, filteredUserToInvite, props]); + }, [currentUserOption, personalDetails, recentReports, userToInvite, translate]); const selectReport = useCallback( (option) => { @@ -196,20 +171,22 @@ function TaskAssigneeSelectorModal(props) { const assigneeChatReport = Task.setAssigneeValue(option.login, option.accountID, report.reportID, OptionsListUtils.isCurrentUser(option)); // Pass through the selected assignee - Task.editTaskAssignee(report, props.session.accountID, option.login, option.accountID, assigneeChatReport); + Task.editTaskAssignee(report, session.accountID, option.login, option.accountID, assigneeChatReport); } Navigation.dismissModalWithReport(report); // If there's no report, we're creating a new task } else if (option.accountID) { - Task.setAssigneeValue(option.login, option.accountID, props.task.shareDestination, OptionsListUtils.isCurrentUser(option)); + Task.setAssigneeValue(option.login, option.accountID, task.shareDestination, OptionsListUtils.isCurrentUser(option)); Navigation.goBack(ROUTES.NEW_TASK); } }, - [props.session.accountID, props.task.shareDestination, report], + [session.accountID, task.shareDestination, report], ); + const handleBackButtonPress = useCallback(() => (lodashGet(route.params, 'reportID') ? Navigation.dismissModal() : Navigation.goBack(ROUTES.NEW_TASK)), [route.params]); + const isOpen = ReportUtils.isOpenTaskReport(report); - const canModifyTask = Task.canModifyTask(report, props.currentUserPersonalDetails.accountID, lodashGet(props.rootParentReportPolicy, 'role', '')); + const canModifyTask = Task.canModifyTask(report, currentUserPersonalDetails.accountID, lodashGet(rootParentReportPolicy, 'role', '')); const isTaskNonEditable = ReportUtils.isTaskReport(report) && (!canModifyTask || !isOpen); return ( @@ -220,21 +197,19 @@ function TaskAssigneeSelectorModal(props) { {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( (lodashGet(props.route.params, 'reportID') ? Navigation.dismissModal() : Navigation.goBack(ROUTES.NEW_TASK))} + title={translate('task.assignee')} + onBackButtonPress={handleBackButtonPress} /> - @@ -248,21 +223,13 @@ TaskAssigneeSelectorModal.propTypes = propTypes; TaskAssigneeSelectorModal.defaultProps = defaultProps; export default compose( - withLocalize, - withCurrentUserPersonalDetails, withOnyx({ reports: { key: ONYXKEYS.COLLECTION.REPORT, }, - betas: { - key: ONYXKEYS.BETAS, - }, task: { key: ONYXKEYS.TASK, }, - session: { - key: ONYXKEYS.SESSION, - }, }), withOnyx({ rootParentReportPolicy: { @@ -271,7 +238,7 @@ export default compose( const rootParentReport = ReportUtils.getRootParentReport(report); return `${ONYXKEYS.COLLECTION.POLICY}${rootParentReport ? rootParentReport.policyID : '0'}`; }, - selector: (policy) => _.pick(policy, ['role']), + selector: (policy) => lodashPick(policy, ['role']), }, }), )(TaskAssigneeSelectorModal);