diff --git a/src/components/DragAndDrop/Provider/types.ts b/src/components/DragAndDrop/Provider/types.ts index b4394056cac5..57d0fb47c637 100644 --- a/src/components/DragAndDrop/Provider/types.ts +++ b/src/components/DragAndDrop/Provider/types.ts @@ -8,7 +8,7 @@ type DragAndDropProviderProps = { isDisabled?: boolean; /** Indicate that users are dragging file or not */ - setIsDraggingOver: (value: boolean) => void; + setIsDraggingOver?: (value: boolean) => void; }; type SetOnDropHandlerCallback = (event: DragEvent) => void; diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 2520520fd467..93ff73bcfaf2 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -40,7 +40,7 @@ type MoneyReportHeaderProps = MoneyReportHeaderOnyxProps & { report: OnyxTypes.Report; /** The policy tied to the money request report */ - policy: OnyxTypes.Policy; + policy: OnyxEntry; }; function MoneyReportHeader({session, policy, chatReport, nextStep, report: moneyRequestReport}: MoneyReportHeaderProps) { @@ -79,8 +79,8 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money // The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on const isWaitingForSubmissionFromCurrentUser = useMemo( - () => chatReport?.isOwnPolicyExpenseChat && !policy.harvesting?.enabled, - [chatReport?.isOwnPolicyExpenseChat, policy.harvesting?.enabled], + () => chatReport?.isOwnPolicyExpenseChat && !policy?.harvesting?.enabled, + [chatReport?.isOwnPolicyExpenseChat, policy?.harvesting?.enabled], ); const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(moneyRequestReport)]; diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 338796cd856e..3dd06b67b637 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -46,10 +46,10 @@ type MoneyRequestHeaderProps = MoneyRequestHeaderOnyxProps & { report: Report; /** The policy which the report is tied to */ - policy: Policy; + policy: OnyxEntry; /** The report action the transaction is tied to from the parent report */ - parentReportAction: ReportAction & OriginalMessageIOU; + parentReportAction: OnyxEntry; }; function MoneyRequestHeader({session, parentReport, report, parentReportAction, transaction, shownHoldUseExplanation = false, policy}: MoneyRequestHeaderProps) { @@ -69,7 +69,11 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && (session?.accountID ?? null) === moneyRequestReport?.managerID; const deleteTransaction = useCallback(() => { - IOU.deleteMoneyRequest(parentReportAction?.originalMessage?.IOUTransactionID ?? '', parentReportAction, true); + if (parentReportAction) { + const iouTransactionID = parentReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction.originalMessage?.IOUTransactionID ?? '' : ''; + IOU.deleteMoneyRequest(iouTransactionID, parentReportAction, true); + } + setIsDeleteModalVisible(false); }, [parentReportAction, setIsDeleteModalVisible]); @@ -83,11 +87,13 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, const canDeleteRequest = isActionOwner && ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) && !isDeletedParentAction; const changeMoneyRequestStatus = () => { + const iouTransactionID = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction.originalMessage?.IOUTransactionID ?? '' : ''; + if (isOnHold) { - IOU.unholdRequest(parentReportAction?.originalMessage?.IOUTransactionID ?? '', report?.reportID); + IOU.unholdRequest(iouTransactionID, report?.reportID); } else { const activeRoute = encodeURIComponent(Navigation.getActiveRouteWithoutParams()); - Navigation.navigate(ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(policy?.type, parentReportAction?.originalMessage?.IOUTransactionID ?? '', report?.reportID, activeRoute)); + Navigation.navigate(ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(policy?.type ?? '', iouTransactionID, report?.reportID, activeRoute)); } }; diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index 4605d27b32dc..306846ad7d99 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -14,7 +14,7 @@ import useTackInputFocus from '@hooks/useTackInputFocus'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; -import type {RootStackParamList} from '@libs/Navigation/types'; +import type {CentralPaneNavigatorParamList, RootStackParamList} from '@libs/Navigation/types'; import toggleTestToolsModal from '@userActions/TestTool'; import CONST from '@src/CONST'; import CustomDevMenu from './CustomDevMenu'; @@ -92,7 +92,7 @@ type ScreenWrapperProps = { * * This is required because transitionEnd event doesn't trigger in the testing environment. */ - navigation?: StackNavigationProp; + navigation?: StackNavigationProp | StackNavigationProp; /** Whether to show offline indicator on wide screens */ shouldShowOfflineIndicatorInWideScreen?: boolean; diff --git a/src/components/withViewportOffsetTop.tsx b/src/components/withViewportOffsetTop.tsx index d3e9b63ad3ee..2b659ac608b5 100644 --- a/src/components/withViewportOffsetTop.tsx +++ b/src/components/withViewportOffsetTop.tsx @@ -40,3 +40,5 @@ export default function withViewportOffsetTop diff --git a/src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts b/src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts index 82410b120df2..8305fa217f79 100644 --- a/src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts +++ b/src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts @@ -1,10 +1,14 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {Report} from '@src/types/onyx'; -export default function reportWithoutHasDraftSelector(report: OnyxEntry) { +type ReportWithoutHasDraft = Omit; + +export default function reportWithoutHasDraftSelector(report: OnyxEntry): OnyxEntry { if (!report) { - return report; + return null; } const {hasDraft, ...reportWithoutHasDraft} = report; return reportWithoutHasDraft; } + +export type {ReportWithoutHasDraft}; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index b12469941fd9..a0b0ae6731a9 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -135,7 +135,7 @@ function isModifiedExpenseAction(reportAction: OnyxEntry): boolean return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE; } -function isWhisperAction(reportAction: OnyxEntry): boolean { +function isWhisperAction(reportAction: OnyxEntry | EmptyObject): boolean { return (reportAction?.whisperedToAccountIDs ?? []).length > 0; } @@ -205,7 +205,7 @@ function isSentMoneyReportAction(reportAction: OnyxEntry): boolean { +function isTransactionThread(parentReportAction: OnyxEntry | EmptyObject): boolean { return ( parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && (parentReportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e3708126322f..8e74eb00ba27 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1230,7 +1230,7 @@ function isMoneyRequest(reportOrID: OnyxEntry | string): boolean { /** * Checks if a report is an IOU or expense report. */ -function isMoneyRequestReport(reportOrID: OnyxEntry | string): boolean { +function isMoneyRequestReport(reportOrID: OnyxEntry | EmptyObject | string): boolean { const report = typeof reportOrID === 'object' ? reportOrID : allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null; return isIOUReport(report) || isExpenseReport(report); } @@ -2806,8 +2806,8 @@ function getReportDescriptionText(report: Report): string { return parser.htmlToText(report.description); } -function getPolicyDescriptionText(policy: Policy): string { - if (!policy.description) { +function getPolicyDescriptionText(policy: OnyxEntry): string { + if (!policy?.description) { return ''; } @@ -4603,7 +4603,7 @@ function getAddWorkspaceRoomOrChatReportErrors(report: OnyxEntry): Error /** * Return true if the Money Request report is marked for deletion. */ -function isMoneyRequestReportPendingDeletion(report: OnyxEntry): boolean { +function isMoneyRequestReportPendingDeletion(report: OnyxEntry | EmptyObject): boolean { if (!isMoneyRequestReport(report)) { return false; } diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index 07bc7f3ed418..619281ac7ecf 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -253,7 +253,7 @@ function signOutAndRedirectToSignIn(shouldReplaceCurrentScreen?: boolean, should * @returns same callback if the action is allowed, otherwise a function that signs out and redirects to sign in */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -function checkIfActionIsAllowed any>(callback: TCallback, isAnonymousAction = false): TCallback | (() => void) { +function checkIfActionIsAllowed any) | void>(callback: TCallback, isAnonymousAction = false): TCallback | (() => void) { if (isAnonymousUser() && !isAnonymousAction) { return () => signOutAndRedirectToSignIn(); } diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index 27c7f3e36fd4..9b3c7bdd74ff 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -100,7 +100,7 @@ function createTaskAndNavigate( assigneeEmail: string, assigneeAccountID = 0, assigneeChatReport: OnyxEntry = null, - policyID = CONST.POLICY.OWNER_EMAIL_FAKE, + policyID: string = CONST.POLICY.OWNER_EMAIL_FAKE, ) { const optimisticTaskReport = ReportUtils.buildOptimisticTaskReport(currentUserAccountID, assigneeAccountID, parentReportID, title, description, policyID); diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.tsx similarity index 74% rename from src/pages/home/HeaderView.js rename to src/pages/home/HeaderView.tsx index 10d2d1414c3a..ad3c40666e76 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.tsx @@ -1,17 +1,15 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {memo, useMemo} from 'react'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import Button from '@components/Button'; import ConfirmModal from '@components/ConfirmModal'; import DisplayNames from '@components/DisplayNames'; +import type {ThreeDotsMenuItem} from '@components/HeaderWithBackButton/types'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import MultipleAvatars from '@components/MultipleAvatars'; import ParentNavigationSubtitle from '@components/ParentNavigationSubtitle'; -import participantPropTypes from '@components/participantPropTypes'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import ReportHeaderSkeletonView from '@components/ReportHeaderSkeletonView'; import SubscriptAvatar from '@components/SubscriptAvatar'; @@ -26,11 +24,11 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import {getGroupChatName} from '@libs/GroupChatUtils'; import * as HeaderUtils from '@libs/HeaderUtils'; import Navigation from '@libs/Navigation/Navigation'; +import type {ReportWithoutHasDraft} from '@libs/OnyxSelectors/reportWithoutHasDraftSelector'; import reportWithoutHasDraftSelector from '@libs/OnyxSelectors/reportWithoutHasDraftSelector'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import reportPropTypes from '@pages/reportPropTypes'; import * as Link from '@userActions/Link'; import * as Report from '@userActions/Report'; import * as Session from '@userActions/Session'; @@ -38,118 +36,100 @@ import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; -const propTypes = { - /** Toggles the navigationMenu open and closed */ - onNavigationMenuButtonClicked: PropTypes.func.isRequired, +type HeaderViewOnyxProps = { + /** URL to the assigned guide's appointment booking calendar */ + guideCalendarLink: OnyxEntry; - /** The report currently being looked at */ - report: reportPropTypes, + /** Current user session */ + session: OnyxEntry; /** Personal details of all the users */ - personalDetails: PropTypes.objectOf(participantPropTypes), - - /** Onyx Props */ - parentReport: reportPropTypes, - - /** URL to the assigned guide's appointment booking calendar */ - guideCalendarLink: PropTypes.string, + personalDetails: OnyxEntry; - /** Current user session */ - session: PropTypes.shape({ - accountID: PropTypes.number, - }), + /** Parent report */ + parentReport: OnyxEntry; /** The current policy of the report */ - policy: PropTypes.shape({ - /** The policy name */ - name: PropTypes.string, + policy: OnyxEntry; +}; - /** The URL for the policy avatar */ - avatar: PropTypes.string, +type HeaderViewProps = HeaderViewOnyxProps & { + /** Toggles the navigationMenu open and closed */ + onNavigationMenuButtonClicked: () => void; - /** The id of the policy */ - id: PropTypes.string, - }), + /** The report currently being looked at */ + report: OnyxTypes.Report; /** The reportID of the request */ - reportID: PropTypes.string.isRequired, + reportID: string; }; -const defaultProps = { - personalDetails: {}, - report: null, - guideCalendarLink: null, - parentReport: {}, - session: { - accountID: 0, - }, - policy: {}, -}; - -function HeaderView(props) { +function HeaderView({report, personalDetails, parentReport, policy, session, reportID, guideCalendarLink, onNavigationMenuButtonClicked}: HeaderViewProps) { const [isDeleteTaskConfirmModalVisible, setIsDeleteTaskConfirmModalVisible] = React.useState(false); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); - const isSelfDM = ReportUtils.isSelfDM(props.report); + const isSelfDM = ReportUtils.isSelfDM(report); // Currently, currentUser is not included in participantAccountIDs, so for selfDM, we need to add the currentUser as participants. - const participants = isSelfDM ? [props.session.accountID] : lodashGet(props.report, 'participantAccountIDs', []); - const participantPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants, props.personalDetails); + const participants = isSelfDM ? [session?.accountID ?? -1] : report?.participantAccountIDs ?? []; + const participantPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants, personalDetails); const isMultipleParticipant = participants.length > 1; const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(participantPersonalDetails, isMultipleParticipant, undefined, isSelfDM); - const isChatThread = ReportUtils.isChatThread(props.report); - const isChatRoom = ReportUtils.isChatRoom(props.report); - const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(props.report); - const isTaskReport = ReportUtils.isTaskReport(props.report); - const reportHeaderData = !isTaskReport && !isChatThread && props.report.parentReportID ? props.parentReport : props.report; + const isChatThread = ReportUtils.isChatThread(report); + const isChatRoom = ReportUtils.isChatRoom(report); + const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); + const isTaskReport = ReportUtils.isTaskReport(report); + const reportHeaderData = !isTaskReport && !isChatThread && report.parentReportID ? parentReport : report; // Use sorted display names for the title for group chats on native small screen widths - const title = ReportUtils.isGroupChat(props.report) ? getGroupChatName(props.report) : ReportUtils.getReportName(reportHeaderData); + const title = ReportUtils.isGroupChat(report) ? getGroupChatName(report) : ReportUtils.getReportName(reportHeaderData); const subtitle = ReportUtils.getChatRoomSubtitle(reportHeaderData); const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(reportHeaderData); - const isConcierge = ReportUtils.hasSingleParticipant(props.report) && _.contains(participants, CONST.ACCOUNT_ID.CONCIERGE); - const parentReportAction = ReportActionsUtils.getParentReportAction(props.report); - const isCanceledTaskReport = ReportUtils.isCanceledTaskReport(props.report, parentReportAction); + const isConcierge = ReportUtils.hasSingleParticipant(report) && participants.includes(CONST.ACCOUNT_ID.CONCIERGE); + const parentReportAction = ReportActionsUtils.getParentReportAction(report); + const isCanceledTaskReport = ReportUtils.isCanceledTaskReport(report, parentReportAction); const isWhisperAction = ReportActionsUtils.isWhisperAction(parentReportAction); - const isUserCreatedPolicyRoom = ReportUtils.isUserCreatedPolicyRoom(props.report); - const isPolicyMember = useMemo(() => !_.isEmpty(props.policy), [props.policy]); - const canLeaveRoom = ReportUtils.canLeaveRoom(props.report, isPolicyMember); - const reportDescription = ReportUtils.getReportDescriptionText(props.report); - const policyName = ReportUtils.getPolicyName(props.report, true); - const policyDescription = ReportUtils.getPolicyDescriptionText(props.policy); - const isPersonalExpenseChat = isPolicyExpenseChat && ReportUtils.isCurrentUserSubmitter(props.report.reportID); + const isUserCreatedPolicyRoom = ReportUtils.isUserCreatedPolicyRoom(report); + const isPolicyMember = useMemo(() => !isEmptyObject(policy), [policy]); + const canLeaveRoom = ReportUtils.canLeaveRoom(report, isPolicyMember); + const reportDescription = ReportUtils.getReportDescriptionText(report); + const policyName = ReportUtils.getPolicyName(report, true); + const policyDescription = ReportUtils.getPolicyDescriptionText(policy); + const isPersonalExpenseChat = isPolicyExpenseChat && ReportUtils.isCurrentUserSubmitter(report.reportID); const shouldShowSubtitle = () => { - if (_.isEmpty(subtitle)) { + if (!subtitle) { return false; } if (isChatRoom) { - return _.isEmpty(reportDescription); + return !reportDescription; } if (isPolicyExpenseChat) { - return _.isEmpty(policyDescription); + return !policyDescription; } return true; }; // We hide the button when we are chatting with an automated Expensify account since it's not possible to contact // these users via alternative means. It is possible to request a call with Concierge so we leave the option for them. - const threeDotMenuItems = []; + const threeDotMenuItems: ThreeDotsMenuItem[] = []; if (isTaskReport && !isCanceledTaskReport) { - const canModifyTask = Task.canModifyTask(props.report, props.session.accountID); + const canModifyTask = Task.canModifyTask(report, session?.accountID ?? -1); // Task is marked as completed - if (ReportUtils.isCompletedTaskReport(props.report) && canModifyTask) { + if (ReportUtils.isCompletedTaskReport(report) && canModifyTask) { threeDotMenuItems.push({ icon: Expensicons.Checkmark, text: translate('task.markAsIncomplete'), - onSelected: Session.checkIfActionIsAllowed(() => Task.reopenTask(props.report)), + onSelected: Session.checkIfActionIsAllowed(() => Task.reopenTask(report)), }); } // Task is not closed - if (props.report.stateNum !== CONST.REPORT.STATE_NUM.APPROVED && props.report.statusNum !== CONST.REPORT.STATUS_NUM.CLOSED && canModifyTask) { + if (report.stateNum !== CONST.REPORT.STATE_NUM.APPROVED && report.statusNum !== CONST.REPORT.STATUS_NUM.CLOSED && canModifyTask) { threeDotMenuItems.push({ icon: Expensicons.Trashcan, text: translate('common.delete'), @@ -159,19 +139,12 @@ function HeaderView(props) { } const join = Session.checkIfActionIsAllowed(() => - Report.updateNotificationPreference( - props.reportID, - props.report.notificationPreference, - CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, - false, - props.report.parentReportID, - props.report.parentReportActionID, - ), + Report.updateNotificationPreference(reportID, report.notificationPreference, CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, false, report.parentReportID, report.parentReportActionID), ); const canJoinOrLeave = !isSelfDM && (isChatThread || isUserCreatedPolicyRoom || canLeaveRoom); - const canJoin = canJoinOrLeave && !isWhisperAction && props.report.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; - const canLeave = canJoinOrLeave && ((isChatThread && props.report.notificationPreference.length) || isUserCreatedPolicyRoom || canLeaveRoom); + const canJoin = canJoinOrLeave && !isWhisperAction && report.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; + const canLeave = canJoinOrLeave && ((isChatThread && !!report.notificationPreference?.length) || isUserCreatedPolicyRoom || canLeaveRoom); if (canJoin) { threeDotMenuItems.push({ icon: Expensicons.ChatBubbles, @@ -179,11 +152,11 @@ function HeaderView(props) { onSelected: join, }); } else if (canLeave) { - const isWorkspaceMemberLeavingWorkspaceRoom = !isChatThread && lodashGet(props.report, 'visibility', '') === CONST.REPORT.VISIBILITY.RESTRICTED && isPolicyMember; + const isWorkspaceMemberLeavingWorkspaceRoom = !isChatThread && report.visibility === CONST.REPORT.VISIBILITY.RESTRICTED && isPolicyMember; threeDotMenuItems.push({ icon: Expensicons.ChatBubbles, text: translate('common.leave'), - onSelected: Session.checkIfActionIsAllowed(() => Report.leaveRoom(props.reportID, isWorkspaceMemberLeavingWorkspaceRoom)), + onSelected: Session.checkIfActionIsAllowed(() => Report.leaveRoom(reportID, isWorkspaceMemberLeavingWorkspaceRoom)), }); } @@ -197,7 +170,7 @@ function HeaderView(props) { ); const renderAdditionalText = () => { - if (shouldShowSubtitle() || isPersonalExpenseChat || _.isEmpty(policyName) || !_.isEmpty(parentNavigationSubtitleData) || isSelfDM) { + if (shouldShowSubtitle() || isPersonalExpenseChat || !policyName || !isEmptyObject(parentNavigationSubtitleData) || isSelfDM) { return null; } return ( @@ -208,28 +181,28 @@ function HeaderView(props) { ); }; - threeDotMenuItems.push(HeaderUtils.getPinMenuItem(props.report)); + threeDotMenuItems.push(HeaderUtils.getPinMenuItem(report)); - if (isConcierge && props.guideCalendarLink) { + if (isConcierge && guideCalendarLink) { threeDotMenuItems.push({ icon: Expensicons.Phone, text: translate('videoChatButtonAndMenu.tooltip'), onSelected: Session.checkIfActionIsAllowed(() => { - Link.openExternalLink(props.guideCalendarLink); + Link.openExternalLink(guideCalendarLink); }), }); } const shouldShowThreeDotsButton = !!threeDotMenuItems.length; - const shouldShowSubscript = ReportUtils.shouldReportShowSubscript(props.report); - const defaultSubscriptSize = ReportUtils.isExpenseRequest(props.report) ? CONST.AVATAR_SIZE.SMALL_NORMAL : CONST.AVATAR_SIZE.DEFAULT; - const icons = ReportUtils.getIcons(reportHeaderData, props.personalDetails); - const brickRoadIndicator = ReportUtils.hasReportNameError(props.report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; + const shouldShowSubscript = ReportUtils.shouldReportShowSubscript(report); + const defaultSubscriptSize = ReportUtils.isExpenseRequest(report) ? CONST.AVATAR_SIZE.SMALL_NORMAL : CONST.AVATAR_SIZE.DEFAULT; + const icons = ReportUtils.getIcons(reportHeaderData, personalDetails); + const brickRoadIndicator = ReportUtils.hasReportNameError(report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; const shouldShowBorderBottom = !isTaskReport || !isSmallScreenWidth; - const shouldDisableDetailPage = ReportUtils.shouldDisableDetailPage(props.report); + const shouldDisableDetailPage = ReportUtils.shouldDisableDetailPage(report); - const isLoading = !props.report || !props.report.reportID || !title; + const isLoading = !report.reportID || !title; return ( {isLoading ? ( - + ) : ( <> {isSmallScreenWidth && ( ReportUtils.navigateToDetailsPage(props.report)} + onPress={() => ReportUtils.navigateToDetailsPage(report)} style={[styles.flexRow, styles.alignItemsCenter, styles.flex1]} disabled={shouldDisableDetailPage} accessibilityLabel={title} @@ -293,10 +266,10 @@ function HeaderView(props) { shouldUseFullTitle={isChatRoom || isPolicyExpenseChat || isChatThread || isTaskReport} renderAdditionalText={renderAdditionalText} /> - {!_.isEmpty(parentNavigationSubtitleData) && ( + {!isEmptyObject(parentNavigationSubtitleData) && ( )} @@ -308,14 +281,14 @@ function HeaderView(props) { {subtitle} )} - {isChatRoom && !_.isEmpty(reportDescription) && _.isEmpty(parentNavigationSubtitleData) && ( + {isChatRoom && !!reportDescription && isEmptyObject(parentNavigationSubtitleData) && ( { - if (ReportUtils.canEditReportDescription(props.report, props.policy)) { - Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(props.reportID)); + if (ReportUtils.canEditReportDescription(report, policy)) { + Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(reportID)); return; } - Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(props.reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID)); }} style={[styles.alignSelfStart, styles.mw100]} accessibilityLabel={translate('reportDescriptionPage.roomDescription')} @@ -328,14 +301,14 @@ function HeaderView(props) { )} - {isPolicyExpenseChat && !_.isEmpty(policyDescription) && _.isEmpty(parentNavigationSubtitleData) && ( + {isPolicyExpenseChat && !!policyDescription && isEmptyObject(parentNavigationSubtitleData) && ( { - if (ReportUtils.canEditPolicyDescription(props.policy)) { - Navigation.navigate(ROUTES.WORKSPACE_PROFILE_DESCRIPTION.getRoute(props.report.policyID)); + if (ReportUtils.canEditPolicyDescription(policy)) { + Navigation.navigate(ROUTES.WORKSPACE_PROFILE_DESCRIPTION.getRoute(report.policyID ?? '')); return; } - Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(props.reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID)); }} style={[styles.alignSelfStart, styles.mw100]} accessibilityLabel={translate('workspace.editor.descriptionInputLabel')} @@ -359,7 +332,7 @@ function HeaderView(props) { )} - {isTaskReport && !isSmallScreenWidth && ReportUtils.isOpenTaskReport(props.report) && } + {isTaskReport && !isSmallScreenWidth && ReportUtils.isOpenTaskReport(report) && } {canJoin && !isSmallScreenWidth && joinButton} {shouldShowThreeDotsButton && ( { setIsDeleteTaskConfirmModalVisible(false); - Session.checkIfActionIsAllowed(Task.deleteTask(props.report)); + Session.checkIfActionIsAllowed(Task.deleteTask(report)); }} onCancel={() => setIsDeleteTaskConfirmModalVisible(false)} title={translate('task.deleteTask')} @@ -391,19 +364,18 @@ function HeaderView(props) { ); } -HeaderView.propTypes = propTypes; + HeaderView.displayName = 'HeaderView'; -HeaderView.defaultProps = defaultProps; export default memo( - withOnyx({ + withOnyx({ guideCalendarLink: { key: ONYXKEYS.ACCOUNT, - selector: (account) => (account && account.guideCalendarLink) || null, + selector: (account) => account?.guideCalendarLink ?? null, initialValue: null, }, parentReport: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID || report.reportID}`, + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID ?? report?.reportID}`, selector: reportWithoutHasDraftSelector, }, session: { diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.tsx similarity index 59% rename from src/pages/home/ReportScreen.js rename to src/pages/home/ReportScreen.tsx index 2e19a2c6a940..f9f73be2ab45 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.tsx @@ -1,10 +1,13 @@ import {useIsFocused} from '@react-navigation/native'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import type {StackScreenProps} from '@react-navigation/stack'; +import lodashIsEqual from 'lodash/isEqual'; import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; +import type {FlatList, ViewStyle} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {WithOnyxInstanceState} from 'react-native-onyx/dist/types'; +import type {LayoutChangeEvent} from 'react-native/Libraries/Types/CoreEventTypes'; import Banner from '@components/Banner'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; @@ -14,154 +17,116 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; import ScreenWrapper from '@components/ScreenWrapper'; import TaskHeaderActionButton from '@components/TaskHeaderActionButton'; -import withCurrentReportID, {withCurrentReportIDDefaultProps, withCurrentReportIDPropTypes} from '@components/withCurrentReportID'; +import withCurrentReportID from '@components/withCurrentReportID'; +import type {CurrentReportIDContextValue} from '@components/withCurrentReportID'; import withViewportOffsetTop from '@components/withViewportOffsetTop'; +import type {ViewportOffsetTopProps} from '@components/withViewportOffsetTop'; import useAppFocusEvent from '@hooks/useAppFocusEvent'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import Timing from '@libs/actions/Timing'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import clearReportNotifications from '@libs/Notification/clearReportNotifications'; import reportWithoutHasDraftSelector from '@libs/OnyxSelectors/reportWithoutHasDraftSelector'; +import type {ReportWithoutHasDraft} from '@libs/OnyxSelectors/reportWithoutHasDraftSelector'; import Performance from '@libs/Performance'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import reportMetadataPropTypes from '@pages/reportMetadataPropTypes'; -import reportPropTypes from '@pages/reportPropTypes'; +import type {CentralPaneNavigatorParamList} from '@navigation/types'; import * as ComposerActions from '@userActions/Composer'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type * as OnyxTypes from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import HeaderView from './HeaderView'; -import reportActionPropTypes from './report/reportActionPropTypes'; import ReportActionsView from './report/ReportActionsView'; import ReportFooter from './report/ReportFooter'; import {ActionListContext, ReactionListContext} from './ReportScreenContext'; +import type {ActionListContextType, ReactionListRef, ScrollPosition} from './ReportScreenContext'; -const propTypes = { - /** Navigation route context info provided by react navigation */ - route: PropTypes.shape({ - /** Route specific parameters used on this screen */ - params: PropTypes.shape({ - /** The ID of the report this screen should display */ - reportID: PropTypes.string, - - /** The reportActionID to scroll to */ - reportActionID: PropTypes.string, - }).isRequired, - }).isRequired, - +type ReportScreenOnyxProps = { /** Tells us if the sidebar has rendered */ - isSidebarLoaded: PropTypes.bool, + isSidebarLoaded: OnyxEntry; - /** The report currently being looked at */ - report: reportPropTypes, + /** Beta features list */ + betas: OnyxEntry; - /** The report metadata loading states */ - reportMetadata: reportMetadataPropTypes, + /** The policies which the user has access to */ + policies: OnyxCollection; - /** All the report actions for this report */ - reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), + /** The account manager report ID */ + accountManagerReportID: OnyxEntry; - /** The report's parentReportAction */ - parentReportAction: PropTypes.shape(reportActionPropTypes), + /** Whether user is leaving the current report */ + userLeavingStatus: OnyxEntry; /** Whether the composer is full size */ - isComposerFullSize: PropTypes.bool, + isComposerFullSize: OnyxEntry; - /** Beta features list */ - betas: PropTypes.arrayOf(PropTypes.string), + /** All the report actions for this report */ + reportActions: OnyxTypes.ReportAction[]; - /** The policies which the user has access to */ - policies: PropTypes.objectOf( - PropTypes.shape({ - /** The policy name */ - name: PropTypes.string, + /** The report currently being looked at */ + report: OnyxEntry; - /** The type of the policy */ - type: PropTypes.string, - }), - ), + /** The report metadata loading states */ + reportMetadata: OnyxEntry; - /** The account manager report ID */ - accountManagerReportID: PropTypes.string, + /** The report's parentReportAction */ + parentReportAction: OnyxEntry; +}; +type OnyxHOCProps = { /** Onyx function that marks the component ready for hydration */ - markReadyForHydration: PropTypes.func, - - /** Whether user is leaving the current report */ - userLeavingStatus: PropTypes.bool, - - viewportOffsetTop: PropTypes.number.isRequired, - ...withCurrentReportIDPropTypes, + markReadyForHydration?: () => void; }; -const defaultProps = { - isSidebarLoaded: false, - reportActions: {}, - parentReportAction: {}, - report: {}, - reportMetadata: { - isLoadingInitialReportActions: true, - isLoadingOlderReportActions: false, - isLoadingNewerReportActions: false, - }, - isComposerFullSize: false, - betas: [], - policies: {}, - accountManagerReportID: null, - userLeavingStatus: false, - markReadyForHydration: null, - ...withCurrentReportIDDefaultProps, -}; +type ReportScreenNavigationProps = StackScreenProps; + +type ReportScreenProps = OnyxHOCProps & ViewportOffsetTopProps & CurrentReportIDContextValue & ReportScreenOnyxProps & ReportScreenNavigationProps; -/** - * Get the currently viewed report ID as number - * - * @param {Object} route - * @param {Object} route.params - * @param {String} route.params.reportID - * @returns {String} - */ -function getReportID(route) { +/** Get the currently viewed report ID as number */ +function getReportID(route: ReportScreenNavigationProps['route']): string { // The report ID is used in an onyx key. If it's an empty string, onyx will return // a collection instead of an individual report. - // We can't use the default value functionality of `lodash.get()` because it only - // provides a default value on `undefined`, and will return an empty string. - // Placing the default value outside of `lodash.get()` is intentional. - return String(lodashGet(route, 'params.reportID') || 0); + return String(route.params?.reportID || 0); } function ReportScreen({ - betas, + betas = [], route, report: reportProp, - reportMetadata, - reportActions, + reportMetadata = { + isLoadingInitialReportActions: true, + isLoadingOlderReportActions: false, + isLoadingNewerReportActions: false, + }, + reportActions = [], parentReportAction, accountManagerReportID, markReadyForHydration, - policies, - isSidebarLoaded, + policies = {}, + isSidebarLoaded = false, viewportOffsetTop, - isComposerFullSize, - errors, - userLeavingStatus, - currentReportID, + isComposerFullSize = false, + userLeavingStatus = false, + currentReportID = '', navigation, -}) { +}: ReportScreenProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); + const isFocused = useIsFocused(); const firstRenderRef = useRef(true); - const flatListRef = useRef(); - const reactionListRef = useRef(); + const flatListRef = useRef(null); + const reactionListRef = useRef(null); /** * Create a lightweight Report so as to keep the re-rendering as light as possible by * passing in only the required props. @@ -171,79 +136,79 @@ function ReportScreen({ * put this into onyx selector as it will be the same. */ const report = useMemo( - () => ({ - lastReadTime: reportProp.lastReadTime, - reportID: reportProp.reportID, - policyID: reportProp.policyID, - lastVisibleActionCreated: reportProp.lastVisibleActionCreated, - statusNum: reportProp.statusNum, - stateNum: reportProp.stateNum, - writeCapability: reportProp.writeCapability, - type: reportProp.type, - errorFields: reportProp.errorFields, - isPolicyExpenseChat: reportProp.isPolicyExpenseChat, - parentReportID: reportProp.parentReportID, - parentReportActionID: reportProp.parentReportActionID, - chatType: reportProp.chatType, - pendingFields: reportProp.pendingFields, - isDeletedParentAction: reportProp.isDeletedParentAction, - reportName: reportProp.reportName, - description: reportProp.description, - managerID: reportProp.managerID, - total: reportProp.total, - nonReimbursableTotal: reportProp.nonReimbursableTotal, - reportFields: reportProp.reportFields, - ownerAccountID: reportProp.ownerAccountID, - currency: reportProp.currency, - participantAccountIDs: reportProp.participantAccountIDs, - isWaitingOnBankAccount: reportProp.isWaitingOnBankAccount, - iouReportID: reportProp.iouReportID, - isOwnPolicyExpenseChat: reportProp.isOwnPolicyExpenseChat, - notificationPreference: reportProp.notificationPreference, - isPinned: reportProp.isPinned, - chatReportID: reportProp.chatReportID, - visibility: reportProp.visibility, - oldPolicyName: reportProp.oldPolicyName, - policyName: reportProp.policyName, - isOptimisticReport: reportProp.isOptimisticReport, - lastMentionedTime: reportProp.lastMentionedTime, + (): OnyxTypes.Report => ({ + lastReadTime: reportProp?.lastReadTime, + reportID: reportProp?.reportID ?? '', + policyID: reportProp?.policyID, + lastVisibleActionCreated: reportProp?.lastVisibleActionCreated, + statusNum: reportProp?.statusNum, + stateNum: reportProp?.stateNum, + writeCapability: reportProp?.writeCapability, + type: reportProp?.type, + errorFields: reportProp?.errorFields, + isPolicyExpenseChat: reportProp?.isPolicyExpenseChat, + parentReportID: reportProp?.parentReportID, + parentReportActionID: reportProp?.parentReportActionID, + chatType: reportProp?.chatType, + pendingFields: reportProp?.pendingFields, + isDeletedParentAction: reportProp?.isDeletedParentAction, + reportName: reportProp?.reportName, + description: reportProp?.description, + managerID: reportProp?.managerID, + total: reportProp?.total, + nonReimbursableTotal: reportProp?.nonReimbursableTotal, + reportFields: reportProp?.reportFields, + ownerAccountID: reportProp?.ownerAccountID, + currency: reportProp?.currency, + participantAccountIDs: reportProp?.participantAccountIDs, + isWaitingOnBankAccount: reportProp?.isWaitingOnBankAccount, + iouReportID: reportProp?.iouReportID, + isOwnPolicyExpenseChat: reportProp?.isOwnPolicyExpenseChat, + notificationPreference: reportProp?.notificationPreference, + isPinned: reportProp?.isPinned, + chatReportID: reportProp?.chatReportID, + visibility: reportProp?.visibility, + oldPolicyName: reportProp?.oldPolicyName, + policyName: reportProp?.policyName, + isOptimisticReport: reportProp?.isOptimisticReport, + lastMentionedTime: reportProp?.lastMentionedTime, }), [ - reportProp.lastReadTime, - reportProp.reportID, - reportProp.policyID, - reportProp.lastVisibleActionCreated, - reportProp.statusNum, - reportProp.stateNum, - reportProp.writeCapability, - reportProp.type, - reportProp.errorFields, - reportProp.isPolicyExpenseChat, - reportProp.parentReportID, - reportProp.parentReportActionID, - reportProp.chatType, - reportProp.pendingFields, - reportProp.isDeletedParentAction, - reportProp.reportName, - reportProp.description, - reportProp.managerID, - reportProp.total, - reportProp.nonReimbursableTotal, - reportProp.reportFields, - reportProp.ownerAccountID, - reportProp.currency, - reportProp.participantAccountIDs, - reportProp.isWaitingOnBankAccount, - reportProp.iouReportID, - reportProp.isOwnPolicyExpenseChat, - reportProp.notificationPreference, - reportProp.isPinned, - reportProp.chatReportID, - reportProp.visibility, - reportProp.oldPolicyName, - reportProp.policyName, - reportProp.isOptimisticReport, - reportProp.lastMentionedTime, + reportProp?.lastReadTime, + reportProp?.reportID, + reportProp?.policyID, + reportProp?.lastVisibleActionCreated, + reportProp?.statusNum, + reportProp?.stateNum, + reportProp?.writeCapability, + reportProp?.type, + reportProp?.errorFields, + reportProp?.isPolicyExpenseChat, + reportProp?.parentReportID, + reportProp?.parentReportActionID, + reportProp?.chatType, + reportProp?.pendingFields, + reportProp?.isDeletedParentAction, + reportProp?.reportName, + reportProp?.description, + reportProp?.managerID, + reportProp?.total, + reportProp?.nonReimbursableTotal, + reportProp?.reportFields, + reportProp?.ownerAccountID, + reportProp?.currency, + reportProp?.participantAccountIDs, + reportProp?.isWaitingOnBankAccount, + reportProp?.iouReportID, + reportProp?.isOwnPolicyExpenseChat, + reportProp?.notificationPreference, + reportProp?.isPinned, + reportProp?.chatReportID, + reportProp?.visibility, + reportProp?.oldPolicyName, + reportProp?.policyName, + reportProp?.isOptimisticReport, + reportProp?.lastMentionedTime, ], ); @@ -251,7 +216,7 @@ function ReportScreen({ const prevUserLeavingStatus = usePrevious(userLeavingStatus); const [isBannerVisible, setIsBannerVisible] = useState(true); const [listHeight, setListHeight] = useState(0); - const [scrollPosition, setScrollPosition] = useState({}); + const [scrollPosition, setScrollPosition] = useState({}); const wasReportAccessibleRef = useRef(false); if (firstRenderRef.current) { @@ -261,23 +226,23 @@ function ReportScreen({ const reportID = getReportID(route); const {reportPendingAction, reportErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); - const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; - const isEmptyChat = useMemo(() => _.isEmpty(reportActions), [reportActions]); + const screenWrapperStyle: ViewStyle[] = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; + const isEmptyChat = useMemo((): boolean => reportActions.length === 0, [reportActions]); // There are no reportActions at all to display and we are still in the process of loading the next set of actions. - const isLoadingInitialReportActions = _.isEmpty(reportActions) && reportMetadata.isLoadingInitialReportActions; - const isOptimisticDelete = lodashGet(report, 'statusNum') === CONST.REPORT.STATUS_NUM.CLOSED; + const isLoadingInitialReportActions = reportActions.length === 0 && !!reportMetadata?.isLoadingInitialReportActions; + const isOptimisticDelete = report.statusNum === CONST.REPORT.STATUS_NUM.CLOSED; const shouldHideReport = !ReportUtils.canAccessReport(report, policies, betas); const isLoading = !reportID || !isSidebarLoaded || PersonalDetailsUtils.isPersonalDetailsEmpty(); - const lastReportAction = useMemo( + const lastReportAction: OnyxEntry = useMemo( () => reportActions.length - ? _.find([...reportActions, parentReportAction], (action) => ReportUtils.canEditReportAction(action) && !ReportActionsUtils.isMoneyRequestAction(action)) - : {}, + ? [...reportActions, parentReportAction].find((action) => ReportUtils.canEditReportAction(action) && !ReportActionsUtils.isMoneyRequestAction(action)) ?? null + : null, [reportActions, parentReportAction], ); const isSingleTransactionView = ReportUtils.isMoneyRequest(report); - const policy = policies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`] || {}; + const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`] ?? null; const isTopMostReportId = currentReportID === getReportID(route); const didSubscribeToReportLeavingEvents = useRef(false); @@ -306,7 +271,6 @@ function ReportScreen({ ); @@ -317,25 +281,21 @@ function ReportScreen({ ); } /** * When false the ReportActionsView will completely unmount and we will show a loader until it returns true. - * - * @returns {Boolean} */ - const isReportReadyForDisplay = useMemo(() => { + const isReportReadyForDisplay = useMemo((): boolean => { const reportIDFromPath = getReportID(route); // This is necessary so that when we are retrieving the next report data from Onyx the ReportActionsView will remount completely const isTransitioning = report && report.reportID !== reportIDFromPath; - return reportIDFromPath !== '' && report.reportID && !isTransitioning; + return reportIDFromPath !== '' && !!report.reportID && !isTransitioning; }, [route, report]); - const isFocused = useIsFocused(); useEffect(() => { if (!report.reportID || !isFocused) { return; @@ -355,7 +315,7 @@ function ReportScreen({ // It possible that we may not have the report object yet in Onyx yet e.g. we navigated to a URL for an accessible report that // is not stored locally yet. If report.reportID exists, then the report has been stored locally and nothing more needs to be done. // If it doesn't exist, then we fetch the report from the API. - if (report.reportID && report.reportID === getReportID(route) && !isLoadingInitialReportActions) { + if (report.reportID === getReportID(route) && !isLoadingInitialReportActions) { return; } @@ -367,7 +327,7 @@ function ReportScreen({ }, []); const chatWithAccountManager = useCallback(() => { - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(accountManagerReportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(accountManagerReportID ?? '')); }, [accountManagerReportID]); // Clear notifications for the current report when it's opened and re-focused @@ -379,6 +339,7 @@ function ReportScreen({ clearReportNotifications(report.reportID); }, [report.reportID, isTopMostReportId]); + useEffect(clearNotifications, [clearNotifications]); useAppFocusEvent(clearNotifications); @@ -391,9 +352,7 @@ function ReportScreen({ ComposerActions.setShouldShowComposeInput(true); }); return () => { - if (interactionTask) { - interactionTask.cancel(); - } + interactionTask?.cancel(); if (!didSubscribeToReportLeavingEvents) { return; } @@ -419,14 +378,14 @@ function ReportScreen({ // Navigate to the Concierge chat if the room was removed from another device (e.g. user leaving a room or removed from a room) if ( // non-optimistic case - (!prevUserLeavingStatus && userLeavingStatus) || + (!prevUserLeavingStatus && !!userLeavingStatus) || // optimistic case - (prevOnyxReportID && + (!!prevOnyxReportID && prevOnyxReportID === routeReportID && !onyxReportID && prevReport.statusNum === CONST.REPORT.STATUS_NUM.OPEN && (report.statusNum === CONST.REPORT.STATUS_NUM.CLOSED || (!report.statusNum && !prevReport.parentReportID && prevReport.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ROOM))) || - ((ReportUtils.isMoneyRequest(prevReport) || ReportUtils.isMoneyRequestReport(prevReport)) && _.isEmpty(report)) + ((ReportUtils.isMoneyRequest(prevReport) || ReportUtils.isMoneyRequestReport(prevReport)) && isEmptyObject(report)) ) { Navigation.dismissModal(); if (Navigation.getTopmostReportId() === prevOnyxReportID) { @@ -456,19 +415,7 @@ function ReportScreen({ fetchReportIfNeeded(); ComposerActions.setShouldShowComposeInput(true); - }, [ - route, - report, - errors, - fetchReportIfNeeded, - prevReport.reportID, - prevUserLeavingStatus, - userLeavingStatus, - prevReport.statusNum, - prevReport.parentReportID, - prevReport.chatType, - prevReport, - ]); + }, [route, report, fetchReportIfNeeded, prevReport.reportID, prevUserLeavingStatus, userLeavingStatus, prevReport.statusNum, prevReport.parentReportID, prevReport.chatType, prevReport]); useEffect(() => { if (!ReportUtils.isValidReportIDFromPath(reportID)) { @@ -479,7 +426,7 @@ function ReportScreen({ // any `pendingFields.createChat` or `pendingFields.addWorkspaceRoom` fields are set to null. // Existing reports created will have empty fields for `pendingFields`. const didCreateReportSuccessfully = !report.pendingFields || (!report.pendingFields.addWorkspaceRoom && !report.pendingFields.createChat); - let interactionTask; + let interactionTask: ReturnType | undefined; if (!didSubscribeToReportLeavingEvents.current && didCreateReportSuccessfully) { interactionTask = InteractionManager.runAfterInteractions(() => { Report.subscribeToReportLeavingEvents(reportID); @@ -495,8 +442,8 @@ function ReportScreen({ }; }, [report, didSubscribeToReportLeavingEvents, reportID]); - const onListLayout = useCallback((e) => { - setListHeight((prev) => lodashGet(e, 'nativeEvent.layout.height', prev)); + const onListLayout = useCallback((event: LayoutChangeEvent) => { + setListHeight((prev) => event.nativeEvent.layout.height ?? prev); if (!markReadyForHydration) { return; } @@ -505,23 +452,23 @@ function ReportScreen({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const reportIDFromParams = lodashGet(route.params, 'reportID'); + const reportIDFromParams = route.params.reportID; // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = useMemo( - () => + (): boolean => (!wasReportAccessibleRef.current && !firstRenderRef.current && !report.reportID && !isOptimisticDelete && - !reportMetadata.isLoadingInitialReportActions && + !reportMetadata?.isLoadingInitialReportActions && !isLoading && !userLeavingStatus) || shouldHideReport || - (reportIDFromParams && !ReportUtils.isValidReportIDFromPath(reportIDFromParams)), + (!!reportIDFromParams && !ReportUtils.isValidReportIDFromPath(reportIDFromParams)), [report, reportMetadata, isLoading, shouldHideReport, isOptimisticDelete, userLeavingStatus, reportIDFromParams], ); - const actionListValue = useMemo(() => ({flatListRef, scrollPosition, setScrollPosition}), [flatListRef, scrollPosition, setScrollPosition]); + const actionListValue = useMemo((): ActionListContextType => ({flatListRef, scrollPosition, setScrollPosition}), [flatListRef, scrollPosition, setScrollPosition]); return ( @@ -535,7 +482,6 @@ function ReportScreen({ )} @@ -594,7 +538,7 @@ function ReportScreen({ `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, - canEvict: false, - selector: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), - }, - report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${getReportID(route)}`, - allowStaleData: true, - selector: reportWithoutHasDraftSelector, - }, - reportMetadata: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_METADATA}${getReportID(route)}`, - initialValue: { - isLoadingInitialReportActions: true, - isLoadingOlderReportActions: false, - isLoadingNewerReportActions: false, +export default withViewportOffsetTop( + withCurrentReportID( + withOnyx( + { + isSidebarLoaded: { + key: ONYXKEYS.IS_SIDEBAR_LOADED, }, - }, - isComposerFullSize: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${getReportID(route)}`, - initialValue: false, - }, - betas: { - key: ONYXKEYS.BETAS, - }, - policies: { - key: ONYXKEYS.COLLECTION.POLICY, - allowStaleData: true, - }, - accountManagerReportID: { - key: ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID, - initialValue: null, - }, - userLeavingStatus: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${getReportID(route)}`, - initialValue: false, - }, - parentReportAction: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : 0}`, - selector: (parentReportActions, props) => { - const parentReportActionID = lodashGet(props, 'report.parentReportActionID'); - if (!parentReportActionID) { - return {}; - } - return lodashGet(parentReportActions, parentReportActionID, {}); + reportActions: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, + canEvict: false, + selector: (reportActions: OnyxEntry) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), + }, + report: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${getReportID(route)}`, + allowStaleData: true, + selector: reportWithoutHasDraftSelector, + }, + reportMetadata: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_METADATA}${getReportID(route)}`, + initialValue: { + isLoadingInitialReportActions: true, + isLoadingOlderReportActions: false, + isLoadingNewerReportActions: false, + }, + }, + isComposerFullSize: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${getReportID(route)}`, + initialValue: false, + }, + betas: { + key: ONYXKEYS.BETAS, + }, + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + allowStaleData: true, + }, + accountManagerReportID: { + key: ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID, + initialValue: null, + }, + userLeavingStatus: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${getReportID(route)}`, + initialValue: false, + }, + parentReportAction: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : 0}`, + selector: (parentReportActions: OnyxEntry, props: WithOnyxInstanceState): OnyxEntry => { + const parentReportActionID = props?.report?.parentReportActionID; + if (!parentReportActionID) { + return null; + } + return parentReportActions?.[parentReportActionID] ?? null; + }, + canEvict: false, }, - canEvict: false, }, - }, - true, - ), -)( - memo( - ReportScreen, - (prevProps, nextProps) => - prevProps.isSidebarLoaded === nextProps.isSidebarLoaded && - _.isEqual(prevProps.reportActions, nextProps.reportActions) && - _.isEqual(prevProps.reportMetadata, nextProps.reportMetadata) && - prevProps.isComposerFullSize === nextProps.isComposerFullSize && - _.isEqual(prevProps.betas, nextProps.betas) && - _.isEqual(prevProps.policies, nextProps.policies) && - prevProps.accountManagerReportID === nextProps.accountManagerReportID && - prevProps.userLeavingStatus === nextProps.userLeavingStatus && - prevProps.currentReportID === nextProps.currentReportID && - prevProps.viewportOffsetTop === nextProps.viewportOffsetTop && - _.isEqual(prevProps.parentReportAction, nextProps.parentReportAction) && - _.isEqual(prevProps.report, nextProps.report), + true, + )( + memo( + ReportScreen, + (prevProps, nextProps) => + prevProps.isSidebarLoaded === nextProps.isSidebarLoaded && + lodashIsEqual(prevProps.reportActions, nextProps.reportActions) && + lodashIsEqual(prevProps.reportMetadata, nextProps.reportMetadata) && + prevProps.isComposerFullSize === nextProps.isComposerFullSize && + lodashIsEqual(prevProps.betas, nextProps.betas) && + lodashIsEqual(prevProps.policies, nextProps.policies) && + prevProps.accountManagerReportID === nextProps.accountManagerReportID && + prevProps.userLeavingStatus === nextProps.userLeavingStatus && + prevProps.currentReportID === nextProps.currentReportID && + prevProps.viewportOffsetTop === nextProps.viewportOffsetTop && + lodashIsEqual(prevProps.parentReportAction, nextProps.parentReportAction) && + lodashIsEqual(prevProps.report, nextProps.report), + ), + ), ), ); diff --git a/src/pages/home/ReportScreenContext.ts b/src/pages/home/ReportScreenContext.ts index 6f177098c2c4..e67bf73f7452 100644 --- a/src/pages/home/ReportScreenContext.ts +++ b/src/pages/home/ReportScreenContext.ts @@ -15,9 +15,11 @@ type ReactionListRef = { type FlatListRefType = RefObject> | null; +type ScrollPosition = {offset?: number}; + type ActionListContextType = { flatListRef: FlatListRefType; - scrollPosition: {offset: number} | null; + scrollPosition: ScrollPosition | null; setScrollPosition: (position: {offset: number}) => void; }; type ReactionListContextType = RefObject | null; @@ -26,4 +28,4 @@ const ActionListContext = createContext({flatListRef: nul const ReactionListContext = createContext(null); export {ActionListContext, ReactionListContext}; -export type {ReactionListRef, ActionListContextType, ReactionListContextType, FlatListRefType, ReactionListAnchor, ReactionListEvent}; +export type {ReactionListRef, ActionListContextType, ReactionListContextType, FlatListRefType, ReactionListAnchor, ReactionListEvent, ScrollPosition}; diff --git a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/floatingMessageCounterContainerPropTypes.js b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/floatingMessageCounterContainerPropTypes.js deleted file mode 100644 index af0f22208457..000000000000 --- a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/floatingMessageCounterContainerPropTypes.js +++ /dev/null @@ -1,11 +0,0 @@ -import PropTypes from 'prop-types'; - -const propTypes = { - /** Styles to be assigned to Container */ - containerStyles: PropTypes.arrayOf(PropTypes.object).isRequired, - - /** Rendered child component */ - children: PropTypes.element.isRequired, -}; - -export default propTypes; diff --git a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.android.js b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.android.tsx similarity index 60% rename from src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.android.js rename to src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.android.tsx index 700a2fb399e4..64391909b197 100644 --- a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.android.js +++ b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.android.tsx @@ -1,18 +1,18 @@ import React from 'react'; import {Animated, View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; -import floatingMessageCounterContainerPropTypes from './floatingMessageCounterContainerPropTypes'; +import type FloatingMessageCounterContainerProps from './types'; -function FloatingMessageCounterContainer(props) { +function FloatingMessageCounterContainer({containerStyles, children}: FloatingMessageCounterContainerProps) { const styles = useThemeStyles(); + return ( - - {props.children} + + {children} ); } -FloatingMessageCounterContainer.propTypes = floatingMessageCounterContainerPropTypes; FloatingMessageCounterContainer.displayName = 'FloatingMessageCounterContainer'; export default FloatingMessageCounterContainer; diff --git a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.js b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.js deleted file mode 100644 index 19123e65cbf2..000000000000 --- a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import {Animated} from 'react-native'; -import useThemeStyles from '@hooks/useThemeStyles'; -import floatingMessageCounterContainerPropTypes from './floatingMessageCounterContainerPropTypes'; - -function FloatingMessageCounterContainer(props) { - const styles = useThemeStyles(); - return ( - - {props.children} - - ); -} - -FloatingMessageCounterContainer.propTypes = floatingMessageCounterContainerPropTypes; -FloatingMessageCounterContainer.displayName = 'FloatingMessageCounterContainer'; - -export default FloatingMessageCounterContainer; diff --git a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.tsx b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.tsx new file mode 100644 index 000000000000..8757d66160c4 --- /dev/null +++ b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import {Animated} from 'react-native'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type FloatingMessageCounterContainerProps from './types'; + +function FloatingMessageCounterContainer({accessibilityHint, containerStyles, children}: FloatingMessageCounterContainerProps) { + const styles = useThemeStyles(); + + return ( + + {children} + + ); +} + +FloatingMessageCounterContainer.displayName = 'FloatingMessageCounterContainer'; + +export default FloatingMessageCounterContainer; diff --git a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/types.ts b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/types.ts new file mode 100644 index 000000000000..cfe791eed79c --- /dev/null +++ b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/types.ts @@ -0,0 +1,12 @@ +import type {StyleProp, ViewStyle} from 'react-native'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; + +type FloatingMessageCounterContainerProps = ChildrenProps & { + /** Styles to be assigned to Container */ + containerStyles?: StyleProp; + + /** Specifies the accessibility hint for the component */ + accessibilityHint?: string; +}; + +export default FloatingMessageCounterContainerProps; diff --git a/src/pages/home/report/FloatingMessageCounter/index.js b/src/pages/home/report/FloatingMessageCounter/index.tsx similarity index 83% rename from src/pages/home/report/FloatingMessageCounter/index.js rename to src/pages/home/report/FloatingMessageCounter/index.tsx index 07138104bf74..d3048848936d 100644 --- a/src/pages/home/report/FloatingMessageCounter/index.js +++ b/src/pages/home/report/FloatingMessageCounter/index.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo} from 'react'; import {Animated, View} from 'react-native'; import Button from '@components/Button'; @@ -12,23 +11,18 @@ import useNativeDriver from '@libs/useNativeDriver'; import CONST from '@src/CONST'; import FloatingMessageCounterContainer from './FloatingMessageCounterContainer'; -const propTypes = { +type FloatingMessageCounterProps = { /** Whether the New Messages indicator is active */ - isActive: PropTypes.bool, + isActive?: boolean; /** Callback to be called when user clicks the New Messages indicator */ - onClick: PropTypes.func, -}; - -const defaultProps = { - isActive: false, - onClick: () => {}, + onClick?: () => void; }; const MARKER_INACTIVE_TRANSLATE_Y = -40; const MARKER_ACTIVE_TRANSLATE_Y = 10; -function FloatingMessageCounter(props) { +function FloatingMessageCounter({isActive = false, onClick = () => {}}: FloatingMessageCounterProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -37,7 +31,6 @@ function FloatingMessageCounter(props) { const show = useCallback(() => { Animated.spring(translateY, { toValue: MARKER_ACTIVE_TRANSLATE_Y, - duration: 80, useNativeDriver, }).start(); }, [translateY]); @@ -45,30 +38,29 @@ function FloatingMessageCounter(props) { const hide = useCallback(() => { Animated.spring(translateY, { toValue: MARKER_INACTIVE_TRANSLATE_Y, - duration: 80, useNativeDriver, }).start(); }, [translateY]); useEffect(() => { - if (props.isActive) { + if (isActive) { show(); } else { hide(); } - }, [props.isActive, show, hide]); + }, [isActive, show, hide]); return (