From fcd95ef1ed7d1fdbb02daeef80f3ff650b78c83b Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 17 Nov 2023 18:04:32 +0500 Subject: [PATCH 001/583] perf: make LHNOptionsList lightweight --- .../LHNOptionsList/LHNOptionsList.js | 99 +++---------------- src/components/LHNOptionsList/OptionRowLHN.js | 5 +- .../LHNOptionsList/OptionRowLHNData.js | 59 +++++------ 3 files changed, 46 insertions(+), 117 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.js b/src/components/LHNOptionsList/LHNOptionsList.js index 5e77947187e9..6f16327af7ee 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.js +++ b/src/components/LHNOptionsList/LHNOptionsList.js @@ -1,16 +1,12 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback} from 'react'; +import React, {memo, useCallback} from 'react'; import {FlatList, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; -import participantPropTypes from '@components/participantPropTypes'; -import transactionPropTypes from '@components/transactionPropTypes'; -import withCurrentReportID, {withCurrentReportIDDefaultProps, withCurrentReportIDPropTypes} from '@components/withCurrentReportID'; -import compose from '@libs/compose'; import * as OptionsListUtils from '@libs/OptionsListUtils'; -import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; -import reportPropTypes from '@pages/reportPropTypes'; +import {getReportActionsByReportID} from '@libs/ReportActionsUtils'; +import {getReportByID} from '@libs/ReportUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -48,56 +44,20 @@ const propTypes = { avatar: PropTypes.string, }), - /** All reports shared with the user */ - reports: PropTypes.objectOf(reportPropTypes), - - /** Array of report actions for this report */ - reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), - /** Indicates which locale the user currently has selected */ preferredLocale: PropTypes.string, - - /** List of users' personal details */ - personalDetails: PropTypes.objectOf(participantPropTypes), - - /** The transaction from the parent report action */ - transactions: PropTypes.objectOf(transactionPropTypes), - /** List of draft comments */ - draftComments: PropTypes.objectOf(PropTypes.string), - ...withCurrentReportIDPropTypes, }; const defaultProps = { style: undefined, shouldDisableFocusOptions: false, - reportActions: {}, - reports: {}, policy: {}, preferredLocale: CONST.LOCALES.DEFAULT, - personalDetails: {}, - transactions: {}, - draftComments: {}, - ...withCurrentReportIDDefaultProps, }; const keyExtractor = (item) => item; -function LHNOptionsList({ - style, - contentContainerStyles, - data, - onSelectRow, - optionMode, - shouldDisableFocusOptions, - reports, - reportActions, - policy, - preferredLocale, - personalDetails, - transactions, - draftComments, - currentReportID, -}) { +function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optionMode, shouldDisableFocusOptions, preferredLocale}) { const styles = useThemeStyles(); /** * This function is used to compute the layout of any given item in our list. Since we know that each item will have the exact same height, this is a performance optimization @@ -131,15 +91,12 @@ function LHNOptionsList({ */ const renderItem = useCallback( ({item: reportID}) => { - const itemFullReport = reports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] || {}; - const itemReportActions = reportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]; - const itemParentReportActions = reportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport.parentReportID}`] || {}; + const itemFullReport = getReportByID(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); + const itemReportActions = getReportActionsByReportID(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`); + const itemParentReportActions = getReportActionsByReportID(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport.parentReportID}`); const itemParentReportAction = itemParentReportActions[itemFullReport.parentReportActionID] || {}; - const itemPolicy = policy[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport.policyID}`] || {}; const transactionID = lodashGet(itemParentReportAction, ['originalMessage', 'IOUTransactionID'], ''); - const itemTransaction = transactionID ? transactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] : {}; - const itemComment = draftComments[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] || ''; - const participantsPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(itemFullReport.participantAccountIDs, personalDetails); + const participantsPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(itemFullReport.participantAccountIDs); return ( ); }, - [currentReportID, draftComments, onSelectRow, optionMode, personalDetails, policy, preferredLocale, reportActions, reports, shouldDisableFocusOptions, transactions], + [onSelectRow, optionMode, preferredLocale, shouldDisableFocusOptions], ); return ( @@ -187,29 +141,8 @@ LHNOptionsList.propTypes = propTypes; LHNOptionsList.defaultProps = defaultProps; LHNOptionsList.displayName = 'LHNOptionsList'; -export default compose( - withCurrentReportID, - withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, - reportActions: { - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - }, - policy: { - key: ONYXKEYS.COLLECTION.POLICY, - }, - preferredLocale: { - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - transactions: { - key: ONYXKEYS.COLLECTION.TRANSACTION, - }, - draftComments: { - key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, - }, - }), -)(LHNOptionsList); +export default withOnyx({ + preferredLocale: { + key: ONYXKEYS.NVP_PREFERRED_LOCALE, + }, +})(memo(LHNOptionsList)); diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index 8420f3db7a1e..fa5e8ed889c5 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -56,6 +56,8 @@ const propTypes = { /** The item that should be rendered */ // eslint-disable-next-line react/forbid-prop-types optionItem: PropTypes.object, + + hasDraft: PropTypes.bool, }; const defaultProps = { @@ -66,6 +68,7 @@ const defaultProps = { optionItem: null, isFocused: false, betas: [], + hasDraft: false, }; function OptionRowLHN(props) { @@ -297,7 +300,7 @@ function OptionRowLHN(props) { /> )} - {optionItem.hasDraftComment && optionItem.isAllowedToComment && ( + {props.hasDraft && optionItem.isAllowedToComment && ( { @@ -83,11 +70,11 @@ function OptionRowLHNData({ const lastReportAction = _.first(sortedReportActions); return TransactionUtils.getLinkedTransaction(lastReportAction); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [fullReport.reportID, receiptTransactions, reportActions]); + }, [reportActions]); const optionItem = useMemo(() => { // Note: ideally we'd have this as a dependent selector in onyx! - const item = SidebarUtils.getOptionData(fullReport, reportActions, personalDetails, preferredLocale, policy, parentReportAction); + const item = SidebarUtils.getOptionData(fullReport, reportActions, preferredLocale, policy, parentReportAction); if (deepEqual(item, optionItemRef.current)) { return optionItemRef.current; } @@ -96,22 +83,15 @@ function OptionRowLHNData({ // Listen parentReportAction to update title of thread report when parentReportAction changed // Listen to transaction to update title of transaction report when transaction changed // eslint-disable-next-line react-hooks/exhaustive-deps - }, [fullReport, linkedTransaction, reportActions, personalDetails, preferredLocale, policy, parentReportAction, transaction]); - - useEffect(() => { - if (!optionItem || optionItem.hasDraftComment || !comment || comment.length <= 0 || isFocused) { - return; - } - Report.setReportWithDraft(reportID, true); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [fullReport, linkedTransaction, reportActions, preferredLocale, policy, parentReportAction, transaction]); return ( ); } @@ -127,4 +107,17 @@ OptionRowLHNData.displayName = 'OptionRowLHNData'; * Thats also why the React.memo is used on the outer component here, as we just * use it to prevent re-renders from parent re-renders. */ -export default React.memo(OptionRowLHNData); +export default withOnyx({ + policy: { + key: ({fullReport}) => `${ONYXKEYS.COLLECTION.POLICY}${fullReport.policyID}`, + initialValue: {}, + }, + comment: { + key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, + initialValue: '', + }, + transaction: { + key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + initialValue: {}, + }, +})(React.memo(OptionRowLHNData)); From eca229585e29d56d55336c76a31f826f33395197 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 17 Nov 2023 18:05:15 +0500 Subject: [PATCH 002/583] feat: add util methods for report and reportactions --- src/libs/ReportActionsUtils.ts | 5 +++++ src/libs/ReportUtils.js | 5 +++++ src/libs/SidebarUtils.ts | 10 ++++++++-- src/libs/actions/Report.js | 5 +++++ 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 4615cac245ea..09def298471e 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -507,6 +507,10 @@ function getReportAction(reportID: string, reportActionID: string): OnyxEntry { + return allReportActions?.[reportID] ?? {}; +} + function getMostRecentReportActionLastModified(): string { // Start with the oldest date possible let mostRecentReportActionLastModified = new Date(0).toISOString(); @@ -642,6 +646,7 @@ export { getNumberOfMoneyRequests, getParentReportAction, getReportAction, + getReportActionsByReportID, getReportPreviewAction, getSortedReportActions, getSortedReportActionsForDisplay, diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 673cb09232de..ef5219720f21 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -4274,7 +4274,12 @@ function shouldDisableWelcomeMessage(report, policy) { return isMoneyRequestReport(report) || isArchivedRoom(report) || !isChatRoom(report) || isChatThread(report) || !PolicyUtils.isPolicyAdmin(policy); } +function getReportByID(reportID) { + return allReports[reportID] || {}; +} + export { + getReportByID, getReportParticipantsTitle, isReportMessageAttachment, findLastAccessedReport, diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 58c4a124335d..350a52978375 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -1,6 +1,6 @@ /* eslint-disable rulesdir/prefer-underscore-method */ import Str from 'expensify-common/lib/str'; -import Onyx from 'react-native-onyx'; +import Onyx, { OnyxEntry } from 'react-native-onyx'; import {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -47,6 +47,12 @@ Onyx.connect({ }, }); +let allPersonalDetails: OnyxEntry>; +Onyx.connect({ + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + callback: (val) => (allPersonalDetails = val), +}); + // Session can remain stale because the only way for the current user to change is to // sign out and sign in, which would clear out all the Onyx // data anyway and cause SidebarLinks to rerender. @@ -295,7 +301,6 @@ type Icon = { function getOptionData( report: Report, reportActions: Record, - personalDetails: Record, preferredLocale: ValueOf, policy: Policy, parentReportAction: ReportAction, @@ -303,6 +308,7 @@ function getOptionData( // When a user signs out, Onyx is cleared. Due to the lazy rendering with a virtual list, it's possible for // this method to be called after the Onyx data has been cleared out. In that case, it's fine to do // a null check here and return early. + const personalDetails = allPersonalDetails; if (!report || !personalDetails) { return; } diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 58d7a9399533..c6c7812a9e43 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -999,6 +999,10 @@ function setReportWithDraft(reportID, hasDraft) { return Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {hasDraft}); } +function getReportDraftStatus(reportID) { + return allReports[reportID] && allReports[reportID].hasDraft; +} + /** * Broadcasts whether or not a user is typing on a report over the report's private pusher channel. * @@ -2505,6 +2509,7 @@ function searchInServer(searchInput) { } export { + getReportDraftStatus, searchInServer, addComment, addAttachment, From def30869f3c385ced52f075b789cd13950d7a5c2 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 17 Nov 2023 18:05:56 +0500 Subject: [PATCH 003/583] perf: add InteractionManager to lazily call methods --- src/libs/Pusher/pusher.ts | 81 +++++++++++----------- src/pages/home/ReportScreen.js | 21 ++++-- src/pages/home/report/ReportActionsList.js | 6 +- src/pages/home/report/ReportActionsView.js | 25 ++++++- 4 files changed, 85 insertions(+), 48 deletions(-) diff --git a/src/libs/Pusher/pusher.ts b/src/libs/Pusher/pusher.ts index dd8af08db229..ec2cefc82bb2 100644 --- a/src/libs/Pusher/pusher.ts +++ b/src/libs/Pusher/pusher.ts @@ -1,5 +1,6 @@ import isObject from 'lodash/isObject'; import {Channel, ChannelAuthorizerGenerator, Options} from 'pusher-js/with-encryption'; +import {InteractionManager} from 'react-native'; import Onyx from 'react-native-onyx'; import {LiteralUnion, ValueOf} from 'type-fest'; import Log from '@libs/Log'; @@ -209,48 +210,50 @@ function bindEventToChannel(channel: Channel | undefined, eventName: PusherEvent */ function subscribe(channelName: string, eventName: PusherEventName, eventCallback: (data: PushJSON) => void = () => {}, onResubscribe = () => {}): Promise { return new Promise((resolve, reject) => { - // We cannot call subscribe() before init(). Prevent any attempt to do this on dev. - if (!socket) { - throw new Error(`[Pusher] instance not found. Pusher.subscribe() + InteractionManager.runAfterInteractions(() => { + // We cannot call subscribe() before init(). Prevent any attempt to do this on dev. + if (!socket) { + throw new Error(`[Pusher] instance not found. Pusher.subscribe() most likely has been called before Pusher.init()`); - } + } - Log.info('[Pusher] Attempting to subscribe to channel', false, {channelName, eventName}); - let channel = getChannel(channelName); - - if (!channel || !channel.subscribed) { - channel = socket.subscribe(channelName); - let isBound = false; - channel.bind('pusher:subscription_succeeded', () => { - // Check so that we do not bind another event with each reconnect attempt - if (!isBound) { - bindEventToChannel(channel, eventName, eventCallback); - resolve(); - isBound = true; - return; - } - - // When subscribing for the first time we register a success callback that can be - // called multiple times when the subscription succeeds again in the future - // e.g. as a result of Pusher disconnecting and reconnecting. This callback does - // not fire on the first subscription_succeeded event. - onResubscribe(); - }); - - channel.bind('pusher:subscription_error', (data: PusherSubscribtionErrorData = {}) => { - const {type, error, status} = data; - Log.hmmm('[Pusher] Issue authenticating with Pusher during subscribe attempt.', { - channelName, - status, - type, - error, + Log.info('[Pusher] Attempting to subscribe to channel', false, {channelName, eventName}); + let channel = getChannel(channelName); + + if (!channel || !channel.subscribed) { + channel = socket.subscribe(channelName); + let isBound = false; + channel.bind('pusher:subscription_succeeded', () => { + // Check so that we do not bind another event with each reconnect attempt + if (!isBound) { + bindEventToChannel(channel, eventName, eventCallback); + resolve(); + isBound = true; + return; + } + + // When subscribing for the first time we register a success callback that can be + // called multiple times when the subscription succeeds again in the future + // e.g. as a result of Pusher disconnecting and reconnecting. This callback does + // not fire on the first subscription_succeeded event. + onResubscribe(); }); - reject(error); - }); - } else { - bindEventToChannel(channel, eventName, eventCallback); - resolve(); - } + + channel.bind('pusher:subscription_error', (data: PusherSubscribtionErrorData = {}) => { + const {type, error, status} = data; + Log.hmmm('[Pusher] Issue authenticating with Pusher during subscribe attempt.', { + channelName, + status, + type, + error, + }); + reject(error); + }); + } else { + bindEventToChannel(channel, eventName, eventCallback); + resolve(); + } + }); }); } diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 0f09b51487ae..04b8034ebfda 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -1,7 +1,7 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; +import {InteractionManager, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import Banner from '@components/Banner'; @@ -276,8 +276,11 @@ function ReportScreen({ useEffect(() => { fetchReportIfNeeded(); - ComposerActions.setShouldShowComposeInput(true); + const interactionTask = InteractionManager.runAfterInteractions(() => { + ComposerActions.setShouldShowComposeInput(true); + }); return () => { + interactionTask.cancel(); if (!didSubscribeToReportLeavingEvents) { return; } @@ -345,10 +348,20 @@ 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; if (!didSubscribeToReportLeavingEvents.current && didCreateReportSuccessfully) { - Report.subscribeToReportLeavingEvents(reportID); - didSubscribeToReportLeavingEvents.current = true; + interactionTask = InteractionManager.runAfterInteractions(() => { + Report.subscribeToReportLeavingEvents(reportID); + didSubscribeToReportLeavingEvents.current = true; + }); } + + return () => { + if (!interactionTask) { + return; + } + interactionTask.cancel(); + }; }, [report, didSubscribeToReportLeavingEvents, reportID]); const onListLayout = useCallback((e) => { diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index dd537959c91f..c0de4b00ea71 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -2,7 +2,7 @@ import {useRoute} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {DeviceEventEmitter} from 'react-native'; +import {DeviceEventEmitter, InteractionManager} from 'react-native'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import _ from 'underscore'; import InvertedFlatList from '@components/InvertedFlatList'; @@ -267,7 +267,9 @@ function ReportActionsList({ if (unsubscribe) { unsubscribe(); } - Report.unsubscribeFromReportChannel(report.reportID); + InteractionManager.runAfterInteractions(() => { + Report.unsubscribeFromReportChannel(report.reportID); + }); }; newActionUnsubscribeMap[report.reportID] = cleanup; diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index b4d21de919bf..872cb2c9292c 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -2,6 +2,7 @@ import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useContext, useEffect, useMemo, useRef} from 'react'; +import {InteractionManager} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import networkPropTypes from '@components/networkPropTypes'; @@ -112,7 +113,15 @@ function ReportActionsView(props) { }; useEffect(() => { - openReportIfNecessary(); + const interactionTask = InteractionManager.runAfterInteractions(() => { + openReportIfNecessary(); + }); + return () => { + if (!interactionTask) { + return; + } + interactionTask.cancel(); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -166,10 +175,20 @@ function ReportActionsView(props) { // any `pendingFields.createChat` or `pendingFields.addWorkspaceRoom` fields are set to null. // Existing reports created will have empty fields for `pendingFields`. const didCreateReportSuccessfully = !props.report.pendingFields || (!props.report.pendingFields.addWorkspaceRoom && !props.report.pendingFields.createChat); + let interactionTask; if (!didSubscribeToReportTypingEvents.current && didCreateReportSuccessfully) { - Report.subscribeToReportTypingEvents(reportID); - didSubscribeToReportTypingEvents.current = true; + interactionTask = InteractionManager.runAfterInteractions(() => { + Report.subscribeToReportTypingEvents(reportID); + didSubscribeToReportTypingEvents.current = true; + }); } + + return () => { + if (!interactionTask) { + return; + } + interactionTask.cancel(); + }; }, [props.report, didSubscribeToReportTypingEvents, reportID]); /** From 7f51b48237e44712c68d1d139077b2de2e2d6b41 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 17 Nov 2023 18:06:23 +0500 Subject: [PATCH 004/583] perf: avoid unnecessary updating onyx --- .../ComposerWithSuggestions.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index e376e8481c0c..faeb904e9852 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -246,15 +246,23 @@ function ComposerWithSuggestions({ Report.setReportWithDraft(reportID, true); } + const hasDraftStatus = Report.getReportDraftStatus(reportID); + + /** + * The extra `!hasDraftStatus` check is to prevent the draft being set + * when the user navigates to the ReportScreen. This doesn't alter anything + * in terms of functionality. + */ // The draft has been deleted. - if (newCommentConverted.length === 0) { + if (newCommentConverted.length === 0 && hasDraftStatus) { Report.setReportWithDraft(reportID, false); } commentRef.current = newCommentConverted; + const isDraftCommentEmpty = getDraftComment(reportID) === ''; if (shouldDebounceSaveComment) { debouncedSaveReportComment(reportID, newCommentConverted); - } else { + } else if (isDraftCommentEmpty && newCommentConverted.length !== 0) { Report.saveReportComment(reportID, newCommentConverted || ''); } if (newCommentConverted) { @@ -504,13 +512,6 @@ function ComposerWithSuggestions({ useEffect(() => { // Scrolls the composer to the bottom and sets the selection to the end, so that longer drafts are easier to edit updateMultilineInputRange(textInputRef.current, shouldAutoFocus); - - if (value.length === 0) { - return; - } - - Report.setReportWithDraft(reportID, true); - // eslint-disable-next-line react-hooks/exhaustive-deps }, []); From cacb92693d8fda10ac9cd33ed47abf9f36288205 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 17 Nov 2023 18:06:33 +0500 Subject: [PATCH 005/583] perf: memoize SidebarLinks --- src/pages/home/sidebar/SidebarLinks.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 5e69be266342..181c8da7b2b1 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -1,6 +1,6 @@ /* eslint-disable rulesdir/onyx-props-must-have-default */ import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useRef} from 'react'; +import React, {memo, useCallback, useEffect, useRef} from 'react'; import {InteractionManager, View} from 'react-native'; import _ from 'underscore'; import LogoComponent from '@assets/images/expensify-wordmark.svg'; @@ -194,5 +194,5 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority SidebarLinks.propTypes = propTypes; SidebarLinks.displayName = 'SidebarLinks'; -export default SidebarLinks; +export default memo(SidebarLinks); export {basePropTypes}; From 518426cf0cc790bec17ad028a4b7048063911520 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 1 Dec 2023 15:33:31 +0500 Subject: [PATCH 006/583] perf: avoid re-creating function and object instances --- src/components/FlatList/index.android.js | 8 ++++---- .../InvertedFlatList/BaseInvertedFlatList.js | 10 ++++++---- src/pages/home/report/ReportActionsList.js | 11 +++++++---- src/pages/home/report/ReportActionsView.js | 10 +++++----- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/components/FlatList/index.android.js b/src/components/FlatList/index.android.js index f7c3da39ed84..caa7273bc141 100644 --- a/src/components/FlatList/index.android.js +++ b/src/components/FlatList/index.android.js @@ -44,6 +44,8 @@ function CustomFlatList(props) { } }, [scrollPosition.offset, props.innerRef]); + const onMomentumScrollEnd = useCallback((event) => setScrollPosition({offset: event.nativeEvent.contentOffset.y}), []); + useFocusEffect( useCallback(() => { onScreenFocus(); @@ -54,10 +56,8 @@ function CustomFlatList(props) { props.onScroll(event)} - onMomentumScrollEnd={(event) => { - setScrollPosition({offset: event.nativeEvent.contentOffset.y}); - }} + onScroll={props.onScroll} + onMomentumScrollEnd={onMomentumScrollEnd} ref={props.innerRef} /> ); diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.js b/src/components/InvertedFlatList/BaseInvertedFlatList.js index 4206d5086a9e..3bb488e947e9 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.js +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.js @@ -17,16 +17,18 @@ const defaultProps = { data: [], }; +const maintainVisibleContentPosition = { + minIndexForVisible: 0, + autoscrollToTopThreshold: AUTOSCROLL_TO_TOP_THRESHOLD, +}; + const BaseInvertedFlatList = forwardRef((props, ref) => ( )); diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 43827341342c..6a480e42ce9c 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -119,6 +119,8 @@ function isMessageUnread(message, lastReadTime) { return Boolean(message && lastReadTime && message.created && lastReadTime < message.created); } +const onScrollToIndexFailed = () => {}; + function ReportActionsList({ report, isLoadingInitialReportActions, @@ -295,11 +297,12 @@ function ReportActionsList({ } }; - const trackVerticalScrolling = (event) => { + const trackVerticalScrolling = useCallback((event) => { scrollingVerticalOffset.current = event.nativeEvent.contentOffset.y; handleUnreadFloatingButton(); onScroll(event); - }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const scrollToBottomAndMarkReportAsRead = () => { reportScrollManager.scrollToBottom(); @@ -394,7 +397,7 @@ function ReportActionsList({ // Native mobile does not render updates flatlist the changes even though component did update called. // To notify there something changes we can use extraData prop to flatlist - const extraData = [isSmallScreenWidth ? currentUnreadMarker : undefined, ReportUtils.isArchivedRoom(report)]; + const extraData = useMemo(() => [isSmallScreenWidth ? currentUnreadMarker : undefined, ReportUtils.isArchivedRoom(report)], [currentUnreadMarker, isSmallScreenWidth, report]); const hideComposer = !ReportUtils.canUserPerformWriteAction(report); const shouldShowReportRecipientLocalTime = ReportUtils.canShowReportRecipientLocalTime(personalDetailsList, report, currentUserPersonalDetails.accountID) && !isComposerFullSize; @@ -471,7 +474,7 @@ function ReportActionsList({ keyboardShouldPersistTaps="handled" onLayout={onLayoutInner} onScroll={trackVerticalScrolling} - onScrollToIndexFailed={() => {}} + onScrollToIndexFailed={onScrollToIndexFailed} extraData={extraData} /> diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 872cb2c9292c..5ee047249853 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -1,7 +1,7 @@ import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useContext, useEffect, useMemo, useRef} from 'react'; +import React, {useCallback, useContext, useEffect, useMemo, useRef} from 'react'; import {InteractionManager} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -195,7 +195,7 @@ function ReportActionsView(props) { * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. */ - const loadOlderChats = () => { + const loadOlderChats = useCallback(() => { // Only fetch more if we are neither already fetching (so that we don't initiate duplicate requests) nor offline. if (props.network.isOffline || props.isLoadingOlderReportActions) { return; @@ -209,7 +209,7 @@ function ReportActionsView(props) { } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments Report.getOlderActions(reportID, oldestReportAction.reportActionID); - }; + }, [props.isLoadingOlderReportActions, props.network.isOffline, props.reportActions, reportID]); /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently @@ -246,7 +246,7 @@ function ReportActionsView(props) { /** * Runs when the FlatList finishes laying out */ - const recordTimeToMeasureItemLayout = () => { + const recordTimeToMeasureItemLayout = useCallback(() => { if (didLayout.current) { return; } @@ -261,7 +261,7 @@ function ReportActionsView(props) { } else { Performance.markEnd(CONST.TIMING.SWITCH_REPORT); } - }; + }, [hasCachedActions]); // Comments have not loaded at all yet do nothing if (!_.size(props.reportActions)) { From e094eba41d470a049ca7bffe2ad623633a401992 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 1 Dec 2023 15:34:11 +0500 Subject: [PATCH 007/583] perf: delay updateComment to run on mount when JS thread is idle --- .../SilentCommentUpdater.js | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater.js b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater.js index 9aa997a892f4..727083c68211 100644 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater.js +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater.js @@ -9,12 +9,6 @@ const propTypes = { /** The comment of the report */ comment: PropTypes.string, - /** The report associated with the comment */ - report: PropTypes.shape({ - /** The ID of the report */ - reportID: PropTypes.string, - }).isRequired, - /** The value of the comment */ value: PropTypes.string.isRequired, @@ -26,6 +20,8 @@ const propTypes = { /** Updates the comment */ updateComment: PropTypes.func.isRequired, + + reportID: PropTypes.string.isRequired, }; const defaultProps = { @@ -38,14 +34,21 @@ const defaultProps = { * re-rendering a UI component for that. That's why the side effect was moved down to a separate component. * @returns {null} */ -function SilentCommentUpdater({comment, commentRef, report, value, updateComment}) { +function SilentCommentUpdater({comment, commentRef, reportID, value, updateComment}) { const prevCommentProp = usePrevious(comment); - const prevReportId = usePrevious(report.reportID); + const prevReportId = usePrevious(reportID); const {preferredLocale} = useLocalize(); const prevPreferredLocale = usePrevious(preferredLocale); useEffect(() => { - updateComment(comment); + /** + * Schedules the callback to run when the main thread is idle. + */ + const callbackID = requestIdleCallback(() => { + updateComment(comment); + }); + + return cancelIdleCallback(callbackID); // eslint-disable-next-line react-hooks/exhaustive-deps -- We need to run this on mount }, []); @@ -56,12 +59,12 @@ function SilentCommentUpdater({comment, commentRef, report, value, updateComment // As the report IDs change, make sure to update the composer comment as we need to make sure // we do not show incorrect data in there (ie. draft of message from other report). - if (preferredLocale === prevPreferredLocale && report.reportID === prevReportId && !shouldSyncComment) { + if (preferredLocale === prevPreferredLocale && reportID === prevReportId && !shouldSyncComment) { return; } updateComment(comment); - }, [prevCommentProp, prevPreferredLocale, prevReportId, comment, preferredLocale, report.reportID, updateComment, value, commentRef]); + }, [prevCommentProp, prevPreferredLocale, prevReportId, comment, preferredLocale, reportID, updateComment, value, commentRef]); return null; } From f215b7fb7c73bfaf708004a1baf26cfa7e1346ab Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 4 Dec 2023 19:03:07 +0500 Subject: [PATCH 008/583] fix: linting and ts issues --- src/components/FlatList/index.android.js | 1 + src/components/LHNOptionsList/OptionRowLHN.js | 1 - .../ReportActionCompose/SilentCommentUpdater.js | 11 ++++++++--- tests/perf-test/SidebarUtils.perf-test.ts | 9 +-------- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/components/FlatList/index.android.js b/src/components/FlatList/index.android.js index caa7273bc141..38717d3362cc 100644 --- a/src/components/FlatList/index.android.js +++ b/src/components/FlatList/index.android.js @@ -44,6 +44,7 @@ function CustomFlatList(props) { } }, [scrollPosition.offset, props.innerRef]); + // eslint-disable-next-line react-hooks/exhaustive-deps const onMomentumScrollEnd = useCallback((event) => setScrollPosition({offset: event.nativeEvent.contentOffset.y}), []); useFocusEffect( diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index fe3a1a0cf48c..0aa6412eefab 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -63,7 +63,6 @@ const defaultProps = { style: null, optionItem: null, isFocused: false, - betas: [], hasDraft: false, }; diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater.js b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater.js index 727083c68211..9b8a2a1497a0 100644 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater.js +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater.js @@ -44,11 +44,16 @@ function SilentCommentUpdater({comment, commentRef, reportID, value, updateComme /** * Schedules the callback to run when the main thread is idle. */ - const callbackID = requestIdleCallback(() => { + let callbackID; + if ('requestIdleCallback' in window) { + callbackID = requestIdleCallback(() => { + updateComment(comment); + }); + } else { updateComment(comment); - }); + } - return cancelIdleCallback(callbackID); + return callbackID && cancelIdleCallback(callbackID); // eslint-disable-next-line react-hooks/exhaustive-deps -- We need to run this on mount }, []); diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index 7f9957232cfb..595f6ad1bbe3 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -3,12 +3,10 @@ import {measureFunction} from 'reassure'; import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {PersonalDetails} from '@src/types/onyx'; import Policy from '@src/types/onyx/Policy'; import Report from '@src/types/onyx/Report'; import ReportAction, {ReportActions} from '@src/types/onyx/ReportAction'; import createCollection from '../utils/collections/createCollection'; -import createPersonalDetails from '../utils/collections/personalDetails'; import createRandomPolicy from '../utils/collections/policies'; import createRandomReportAction from '../utils/collections/reportActions'; import createRandomReport from '../utils/collections/reports'; @@ -38,11 +36,6 @@ const reportActions = createCollection( (index) => createRandomReportAction(index), ); -const personalDetails = createCollection( - (item) => item.accountID, - (index) => createPersonalDetails(index), -); - const mockedResponseMap = getMockedReports(5000) as Record<`${typeof ONYXKEYS.COLLECTION.REPORT}`, Report>; const runs = CONST.PERFORMANCE_TESTS.RUNS; @@ -57,7 +50,7 @@ test('getOptionData on 5k reports', async () => { }); await waitForBatchedUpdates(); - await measureFunction(() => SidebarUtils.getOptionData(report, reportActions, personalDetails, preferredLocale, policy, parentReportAction), {runs}); + await measureFunction(() => SidebarUtils.getOptionData(report, reportActions, preferredLocale, policy, parentReportAction), {runs}); }); test('getOrderedReportIDs on 5k reports', async () => { From 49fedcc08692e26bfcef84cfdc208c6eee8c9981 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 5 Dec 2023 18:12:52 +0500 Subject: [PATCH 009/583] perf: apply perf improvements from pr 30168 --- src/components/AnonymousReportFooter.tsx | 11 ++- src/components/MoneyReportHeader.js | 3 + src/components/MoneyRequestHeader.js | 3 + src/libs/PersonalDetailsUtils.js | 9 +++ src/pages/home/ReportScreen.js | 53 ++++++++----- .../ComposerWithSuggestions.js | 60 +++++++------- .../ReportActionCompose.js | 25 +++--- src/pages/home/report/ReportActionsList.js | 2 +- .../report/ReportActionsListItemRenderer.js | 52 ++++++++++-- src/pages/home/report/ReportActionsView.js | 79 ++++++------------- src/pages/home/report/ReportFooter.js | 38 ++++++--- 11 files changed, 197 insertions(+), 138 deletions(-) diff --git a/src/components/AnonymousReportFooter.tsx b/src/components/AnonymousReportFooter.tsx index 65dc813a829d..e6e47a0d5f7a 100644 --- a/src/components/AnonymousReportFooter.tsx +++ b/src/components/AnonymousReportFooter.tsx @@ -1,14 +1,15 @@ import React from 'react'; import {Text, View} from 'react-native'; -import {OnyxCollection} from 'react-native-onyx'; import {OnyxEntry} from 'react-native-onyx/lib/types'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@styles/useThemeStyles'; import * as Session from '@userActions/Session'; -import {PersonalDetails, Report} from '@src/types/onyx'; +import CONST from '@src/CONST'; +import {Report} from '@src/types/onyx'; import AvatarWithDisplayName from './AvatarWithDisplayName'; import Button from './Button'; import ExpensifyWordmark from './ExpensifyWordmark'; +import {usePersonalDetails} from './OnyxProvider'; type AnonymousReportFooterProps = { /** The report currently being looked at */ @@ -16,14 +17,12 @@ type AnonymousReportFooterProps = { /** Whether the small screen size layout should be used */ isSmallSizeLayout?: boolean; - - /** Personal details of all the users */ - personalDetails: OnyxCollection; }; -function AnonymousReportFooter({isSmallSizeLayout = false, personalDetails, report}: AnonymousReportFooterProps) { +function AnonymousReportFooter({isSmallSizeLayout = false, report}: AnonymousReportFooterProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; return ( diff --git a/src/components/MoneyReportHeader.js b/src/components/MoneyReportHeader.js index 880e46b2592a..c5fab09e1711 100644 --- a/src/components/MoneyReportHeader.js +++ b/src/components/MoneyReportHeader.js @@ -211,5 +211,8 @@ export default compose( session: { key: ONYXKEYS.SESSION, }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, }), )(MoneyReportHeader); diff --git a/src/components/MoneyRequestHeader.js b/src/components/MoneyRequestHeader.js index 178163f6569f..51e8143a5c76 100644 --- a/src/components/MoneyRequestHeader.js +++ b/src/components/MoneyRequestHeader.js @@ -174,6 +174,9 @@ export default compose( key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '0'}`, canEvict: false, }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, }), // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file withOnyx({ diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js index 560480dcec9d..88585b1cb360 100644 --- a/src/libs/PersonalDetailsUtils.js +++ b/src/libs/PersonalDetailsUtils.js @@ -197,7 +197,16 @@ function getFormattedAddress(privatePersonalDetails) { return formattedAddress.trim().replace(/,$/, ''); } +/** + * Whether personal details is empty + * @returns {Boolean} true if personal details is empty + */ +function isPersonalDetailsEmpty() { + return !personalDetails.length; +} + export { + isPersonalDetailsEmpty, getDisplayNameOrDefault, getPersonalDetailsByIDs, getAccountIDsByLogins, diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index add20882d6e4..3110bc031647 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -1,6 +1,6 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -21,9 +21,9 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import reportWithoutHasDraftSelector from '@libs/OnyxSelectors/reportWithoutHasDraftSelector'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import personalDetailsPropType from '@pages/personalDetailsPropType'; import reportMetadataPropTypes from '@pages/reportMetadataPropTypes'; import reportPropTypes from '@pages/reportPropTypes'; import useThemeStyles from '@styles/useThemeStyles'; @@ -83,9 +83,6 @@ const propTypes = { /** The account manager report ID */ accountManagerReportID: PropTypes.string, - /** All of the personal details for everyone */ - personalDetails: PropTypes.objectOf(personalDetailsPropType), - /** Onyx function that marks the component ready for hydration */ markReadyForHydration: PropTypes.func, @@ -112,7 +109,6 @@ const defaultProps = { policies: {}, accountManagerReportID: null, userLeavingStatus: false, - personalDetails: {}, markReadyForHydration: null, ...withCurrentReportIDDefaultProps, }; @@ -141,7 +137,6 @@ function ReportScreen({ reportMetadata, reportActions, accountManagerReportID, - personalDetails, markReadyForHydration, policies, isSidebarLoaded, @@ -168,16 +163,24 @@ function ReportScreen({ const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; + const isEmptyChat = useMemo(() => _.isEmpty(reportActions), [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 isLoadingInitialReportActions = isEmptyChat && reportMetadata.isLoadingReportActions; const isOptimisticDelete = lodashGet(report, 'statusNum') === CONST.REPORT.STATUS.CLOSED; const shouldHideReport = !ReportUtils.canAccessReport(report, policies, betas); - const isLoading = !reportID || !isSidebarLoaded || _.isEmpty(personalDetails); + const isLoading = !reportID || !isSidebarLoaded || PersonalDetailsUtils.isPersonalDetailsEmpty(); const parentReportAction = ReportActionsUtils.getParentReportAction(report); + const lastReportAction = useMemo( + () => + reportActions.length + ? _.find([...reportActions, parentReportAction], (action) => ReportUtils.canEditReportAction(action) && !ReportActionsUtils.isMoneyRequestAction(action)) + : {}, + [reportActions, parentReportAction], + ); const isSingleTransactionView = ReportUtils.isMoneyRequest(report); const policy = policies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`] || {}; @@ -193,7 +196,6 @@ function ReportScreen({ ); @@ -203,7 +205,6 @@ function ReportScreen({ @@ -215,7 +216,6 @@ function ReportScreen({ @@ -455,13 +455,12 @@ function ReportScreen({ {isReportReadyForDisplay ? ( ) : ( @@ -520,9 +519,6 @@ export default compose( key: ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID, initialValue: null, }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, userLeavingStatus: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${getReportID(route)}`, initialValue: false, @@ -530,4 +526,23 @@ export default compose( }, true, ), -)(ReportScreen); +)( + 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.report.reportID === nextProps.report.reportID && + prevProps.report.policyID === nextProps.report.policyID && + prevProps.report.isOptimisticReport === nextProps.report.isOptimisticReport && + prevProps.report.statusNum === nextProps.report.statusNum && + _.isEqual(prevProps.report.pendingFields, nextProps.report.pendingFields) && + prevProps.currentReportID === nextProps.currentReportID, + ), +); diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 078979fff7eb..21e8d7bc8d6e 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -65,14 +65,15 @@ function ComposerWithSuggestions({ // Onyx modal, preferredSkinTone, - parentReportActions, + parentReportAction, numberOfLines, // HOCs isKeyboardShown, // Props: Report reportID, - report, - reportActions, + includeChronos, + isEmptyChat, + lastReportAction, // Focus onFocus, onBlur, @@ -119,9 +120,7 @@ function ComposerWithSuggestions({ const {isSmallScreenWidth} = useWindowDimensions(); const maxComposerLines = isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES; - const isEmptyChat = useMemo(() => _.size(reportActions) === 1, [reportActions]); - const parentAction = ReportActionsUtils.getParentReportAction(report); - const shouldAutoFocus = !modal.isVisible && (shouldFocusInputOnScreenFocus || (isEmptyChat && !ReportActionsUtils.isTransactionThread(parentAction))) && shouldShowComposeInput; + const shouldAutoFocus = !modal.isVisible && (shouldFocusInputOnScreenFocus || (isEmptyChat && !ReportActionsUtils.isTransactionThread(parentReportAction))) && shouldShowComposeInput; const valueRef = useRef(value); valueRef.current = value; @@ -355,21 +354,15 @@ function ComposerWithSuggestions({ // Trigger the edit box for last sent message if ArrowUp is pressed and the comment is empty and Chronos is not in the participants const valueLength = valueRef.current.length; - if (e.key === CONST.KEYBOARD_SHORTCUTS.ARROW_UP.shortcutKey && textInputRef.current.selectionStart === 0 && valueLength === 0 && !ReportUtils.chatIncludesChronos(report)) { + if (e.key === CONST.KEYBOARD_SHORTCUTS.ARROW_UP.shortcutKey && textInputRef.current.selectionStart === 0 && valueLength === 0 && !includeChronos) { e.preventDefault(); - const parentReportActionID = lodashGet(report, 'parentReportActionID', ''); - const parentReportAction = lodashGet(parentReportActions, [parentReportActionID], {}); - const lastReportAction = _.find( - [...reportActions, parentReportAction], - (action) => ReportUtils.canEditReportAction(action) && !ReportActionsUtils.isMoneyRequestAction(action), - ); if (lastReportAction) { Report.saveReportActionDraft(reportID, lastReportAction, _.last(lastReportAction.message).html); } } }, - [isKeyboardShown, isSmallScreenWidth, parentReportActions, report, reportActions, reportID, handleSendMessage, suggestionsRef, valueRef], + [isSmallScreenWidth, isKeyboardShown, suggestionsRef, includeChronos, handleSendMessage, lastReportAction, reportID], ); const onSelectionChange = useCallback( @@ -526,6 +519,27 @@ function ComposerWithSuggestions({ [blur, focus, prepareCommentAndResetComposer, replaceSelectionWithText], ); + const onLayout = useCallback( + (e) => { + const composerLayoutHeight = e.nativeEvent.layout.height; + if (composerHeight === composerLayoutHeight) { + return; + } + setComposerHeight(composerLayoutHeight); + }, + [composerHeight], + ); + + const onClear = useCallback(() => { + setTextInputShouldClear(false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const onChangeText = useCallback((text) => { + updateComment(text, true); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( <> @@ -536,7 +550,7 @@ function ComposerWithSuggestions({ ref={setTextInputRef} placeholder={inputPlaceholder} placeholderTextColor={theme.placeholderText} - onChangeText={(commentValue) => updateComment(commentValue, true)} + onChangeText={onChangeText} onKeyPress={triggerHotkeyActions} textAlignVertical="top" style={[styles.textInputCompose, isComposerFullSize ? styles.textInputFullCompose : styles.textInputCollapseCompose]} @@ -546,7 +560,7 @@ function ComposerWithSuggestions({ onClick={setShouldBlockSuggestionCalcToFalse} onPasteFile={displayFileInModal} shouldClear={textInputShouldClear} - onClear={() => setTextInputShouldClear(false)} + onClear={onClear} isDisabled={isBlockedFromConcierge || disabled} isReportActionCompose selection={selection} @@ -559,13 +573,7 @@ function ComposerWithSuggestions({ numberOfLines={numberOfLines} onNumberOfLinesChange={updateNumberOfLines} shouldCalculateCaretPosition - onLayout={(e) => { - const composerLayoutHeight = e.nativeEvent.layout.height; - if (composerHeight === composerLayoutHeight) { - return; - } - setComposerHeight(composerLayoutHeight); - }} + onLayout={onLayout} onScroll={hideSuggestionMenu} /> @@ -588,7 +596,6 @@ function ComposerWithSuggestions({ `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, - canEvict: false, - initWithStoredValues: false, - }, }), )(ComposerWithSuggestionsWithRef); diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index 89e1ecb6156b..b4e207ae7bdf 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -20,11 +20,11 @@ import compose from '@libs/compose'; import getDraftComment from '@libs/ComposerUtils/getDraftComment'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import getModalState from '@libs/getModalState'; +import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import updatePropsPaperWorklet from '@libs/updatePropsPaperWorklet'; import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside'; import ParticipantLocalTime from '@pages/home/report/ParticipantLocalTime'; -import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; import ReportDropUI from '@pages/home/report/ReportDropUI'; import ReportTypingIndicator from '@pages/home/report/ReportTypingIndicator'; import reportPropTypes from '@pages/reportPropTypes'; @@ -45,9 +45,6 @@ const propTypes = { /** The ID of the report actions will be created for */ reportID: PropTypes.string.isRequired, - /** Array of report actions for this report */ - reportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)), - /** The report currently being looked at */ report: reportPropTypes, @@ -105,7 +102,8 @@ function ReportActionCompose({ pendingAction, report, reportID, - reportActions, + isEmptyChat, + lastReportAction, listHeight, shouldShowComposeInput, isReportReadyForDisplay, @@ -159,7 +157,9 @@ function ReportActionCompose({ [personalDetails, report, currentUserPersonalDetails.accountID, isComposerFullSize], ); - const isBlockedFromConcierge = useMemo(() => ReportUtils.chatIncludesConcierge(report) && User.isBlockedFromConcierge(blockedFromConcierge), [report, blockedFromConcierge]); + const includesConcierge = useMemo(() => ReportUtils.chatIncludesConcierge({participantAccountIDs: report.participantAccountIDs}), [report.participantAccountIDs]); + const userBlockedFromConcierge = useMemo(() => User.isBlockedFromConcierge(blockedFromConcierge), [blockedFromConcierge]); + const isBlockedFromConcierge = useMemo(() => includesConcierge && userBlockedFromConcierge, [includesConcierge, userBlockedFromConcierge]); // If we are on a small width device then don't show last 3 items from conciergePlaceholderOptions const conciergePlaceholderRandomIndex = useMemo( @@ -170,8 +170,8 @@ function ReportActionCompose({ // Placeholder to display in the chat input. const inputPlaceholder = useMemo(() => { - if (ReportUtils.chatIncludesConcierge(report)) { - if (User.isBlockedFromConcierge(blockedFromConcierge)) { + if (includesConcierge) { + if (userBlockedFromConcierge) { return translate('reportActionCompose.blockedFromConcierge'); } @@ -179,7 +179,7 @@ function ReportActionCompose({ } return translate('reportActionCompose.writeSomething'); - }, [report, blockedFromConcierge, translate, conciergePlaceholderRandomIndex]); + }, [includesConcierge, translate, userBlockedFromConcierge, conciergePlaceholderRandomIndex]); const focus = () => { if (composerRef === null || composerRef.current === null) { @@ -318,6 +318,7 @@ function ReportActionCompose({ const hasReportRecipient = _.isObject(reportRecipient) && !_.isEmpty(reportRecipient); const isSendDisabled = isCommentEmpty || isBlockedFromConcierge || disabled || hasExceededMaxCommentLength; + const parentReportAction = ReportActionsUtils.getParentReportAction(report); const handleSendMessage = useCallback(() => { 'worklet'; @@ -392,7 +393,11 @@ function ReportActionCompose({ isNextModalWillOpenRef={isNextModalWillOpenRef} reportID={reportID} report={report} - reportActions={reportActions} + parentReportID={report.parentReportID} + includesChronos={ReportUtils.chatIncludesChronos(report)} + parentReportAction={parentReportAction} + isEmptyChat={isEmptyChat} + lastReportAction={lastReportAction} isMenuVisible={isMenuVisible} inputPlaceholder={inputPlaceholder} isComposerFullSize={isComposerFullSize} diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 6a480e42ce9c..d0d7248c387b 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -386,7 +386,7 @@ function ReportActionsList({ report={report} linkedReportActionID={linkedReportActionID} hasOutstandingIOU={hasOutstandingIOU} - sortedReportActions={sortedReportActions} + displayAsGroup={ReportActionsUtils.isConsecutiveActionMadeByPreviousActor(sortedReportActions, index)} mostRecentIOUReportActionID={mostRecentIOUReportActionID} shouldHideThreadDividerLine={shouldHideThreadDividerLine} shouldDisplayNewMarker={shouldDisplayNewMarker(reportAction, index)} diff --git a/src/pages/home/report/ReportActionsListItemRenderer.js b/src/pages/home/report/ReportActionsListItemRenderer.js index 0273a3f31805..796e63776232 100644 --- a/src/pages/home/report/ReportActionsListItemRenderer.js +++ b/src/pages/home/report/ReportActionsListItemRenderer.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React, {memo} from 'react'; +import React, {memo, useMemo} from 'react'; import _ from 'underscore'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -22,9 +22,6 @@ const propTypes = { /** Whether the option has an outstanding IOU */ hasOutstandingIOU: PropTypes.bool, - /** Sorted actions prepared for display */ - sortedReportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)).isRequired, - /** The ID of the most recent IOU report action connected with the shown report */ mostRecentIOUReportActionID: PropTypes.string, @@ -36,6 +33,8 @@ const propTypes = { /** Linked report action ID */ linkedReportActionID: PropTypes.string, + + displayAsGroup: PropTypes.bool.isRequired, }; const defaultProps = { @@ -49,7 +48,7 @@ function ReportActionsListItemRenderer({ index, report, hasOutstandingIOU, - sortedReportActions, + displayAsGroup, mostRecentIOUReportActionID, shouldHideThreadDividerLine, shouldDisplayNewMarker, @@ -60,6 +59,45 @@ function ReportActionsListItemRenderer({ ReportUtils.isChatThread(report) && !ReportActionsUtils.isTransactionThread(ReportActionsUtils.getParentReportAction(report)); + /** + * Create a lightweight ReportAction so as to keep the re-rendering as light as possible by + * passing in only the required props. + */ + const action = useMemo( + () => ({ + reportActionID: reportAction.reportActionID, + message: reportAction.message, + pendingAction: reportAction.pendingAction, + actionName: reportAction.actionName, + errors: reportAction.errors, + originalMessage: reportAction.originalMessage, + childCommenterCount: reportAction.childCommenterCount, + linkMetadata: reportAction.linkMetadata, + childReportID: reportAction.childReportID, + childLastVisibleActionCreated: reportAction.childLastVisibleActionCreated, + whisperedToAccountIDs: reportAction.whisperedToAccountIDs, + error: reportAction.error, + created: reportAction.created, + actorAccountID: reportAction.actorAccountID, + }), + [ + reportAction.actionName, + reportAction.childCommenterCount, + reportAction.childLastVisibleActionCreated, + reportAction.childReportID, + reportAction.created, + reportAction.error, + reportAction.errors, + reportAction.linkMetadata, + reportAction.message, + reportAction.originalMessage, + reportAction.pendingAction, + reportAction.reportActionID, + reportAction.whisperedToAccountIDs, + reportAction.actorAccountID, + ], + ); + return shouldDisplayParentAction ? ( { const wasLoginChangedDetected = prevAuthTokenType === 'anonymousAccount' && !props.session.authTokenType; @@ -167,7 +167,7 @@ function ReportActionsView(props) { // update ref with current state prevIsSmallScreenWidthRef.current = props.isSmallScreenWidth; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.isSmallScreenWidth, props.report, props.reportActions, isReportFullyVisible]); + }, [props.isSmallScreenWidth, props.reportActions, isReportFullyVisible]); useEffect(() => { // Ensures subscription event succeeds when the report/workspace room is created optimistically. @@ -189,7 +189,7 @@ function ReportActionsView(props) { } interactionTask.cancel(); }; - }, [props.report, didSubscribeToReportTypingEvents, reportID]); + }, [props.report.pendingFields, didSubscribeToReportTypingEvents, reportID]); /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently @@ -263,6 +263,20 @@ function ReportActionsView(props) { } }, [hasCachedActions]); + /** + * Create a lightweight Report so as to keep the re-rendering as light as possible by + * passing in only the required props. + */ + const report = useMemo( + () => ({ + lastReadTime: props.report.lastReadTime, + reportID: props.report.reportID, + policyID: props.report.policyID, + lastVisibleActionCreated: props.report.lastVisibleActionCreated, + }), + [props.report.lastReadTime, props.report.reportID, props.report.policyID, props.report.lastVisibleActionCreated], + ); + // Comments have not loaded at all yet do nothing if (!_.size(props.reportActions)) { return null; @@ -271,7 +285,7 @@ function ReportActionsView(props) { return ( <> {}, pendingAction: null, - personalDetails: {}, shouldShowComposeInput: true, shouldDisableCompose: false, listHeight: 0, isReportReadyForDisplay: true, + lastReportAction: null, + isEmptyChat: true, }; function ReportFooter(props) { @@ -81,7 +79,6 @@ function ReportFooter(props) { )} {isArchivedRoom && } @@ -96,8 +93,8 @@ function ReportFooter(props) { + isEqual(prevProps.report, nextProps.report) && + prevProps.pendingAction === nextProps.pendingAction && + prevProps.listHeight === nextProps.listHeight && + prevProps.isComposerFullSize === nextProps.isComposerFullSize && + prevProps.isEmptyChat === nextProps.isEmptyChat && + prevProps.lastReportAction === nextProps.lastReportAction && + prevProps.shouldShowComposeInput === nextProps.shouldShowComposeInput && + prevProps.windowWidth === nextProps.windowWidth && + prevProps.isSmallScreenWidth === nextProps.isSmallScreenWidth && + prevProps.isReportReadyForDisplay === nextProps.isReportReadyForDisplay, + ), +); From 41d47dc5c2f8cb41613d5e3f289cb00a50424350 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 7 Dec 2023 18:05:22 +0500 Subject: [PATCH 010/583] fix: personal details --- src/components/AnonymousReportFooter.tsx | 24 ++++++++++++------- src/components/AvatarWithDisplayName.tsx | 13 ++++++---- .../LHNOptionsList/LHNOptionsList.js | 14 ++++++++--- .../LHNOptionsList/OptionRowLHNData.js | 9 +++++-- src/components/MoneyReportHeader.js | 11 +++------ src/components/MoneyRequestHeader.js | 11 +++------ src/libs/SidebarUtils.ts | 10 ++------ tests/perf-test/SidebarUtils.perf-test.ts | 9 ++++++- 8 files changed, 58 insertions(+), 43 deletions(-) diff --git a/src/components/AnonymousReportFooter.tsx b/src/components/AnonymousReportFooter.tsx index e6e47a0d5f7a..9c396db8e517 100644 --- a/src/components/AnonymousReportFooter.tsx +++ b/src/components/AnonymousReportFooter.tsx @@ -1,17 +1,22 @@ import React from 'react'; import {Text, View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; import {OnyxEntry} from 'react-native-onyx/lib/types'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@styles/useThemeStyles'; import * as Session from '@userActions/Session'; -import CONST from '@src/CONST'; -import {Report} from '@src/types/onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {Policy, Report} from '@src/types/onyx'; import AvatarWithDisplayName from './AvatarWithDisplayName'; import Button from './Button'; import ExpensifyWordmark from './ExpensifyWordmark'; -import {usePersonalDetails} from './OnyxProvider'; -type AnonymousReportFooterProps = { +type AnonymousReportFooterPropsWithOnyx = { + /** The policy which the user has access to and which the report is tied to */ + policy: OnyxEntry; +}; + +type AnonymousReportFooterProps = AnonymousReportFooterPropsWithOnyx & { /** The report currently being looked at */ report: OnyxEntry; @@ -19,19 +24,18 @@ type AnonymousReportFooterProps = { isSmallSizeLayout?: boolean; }; -function AnonymousReportFooter({isSmallSizeLayout = false, report}: AnonymousReportFooterProps) { +function AnonymousReportFooter({isSmallSizeLayout = false, report, policy}: AnonymousReportFooterProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; return ( @@ -56,4 +60,8 @@ function AnonymousReportFooter({isSmallSizeLayout = false, report}: AnonymousRep AnonymousReportFooter.displayName = 'AnonymousReportFooter'; -export default AnonymousReportFooter; +export default withOnyx({ + policy: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`, + }, +})(AnonymousReportFooter); diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index 9229cb80cf4c..6ffc7bdadd8b 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -1,6 +1,6 @@ import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; -import {OnyxCollection, OnyxEntry, withOnyx} from 'react-native-onyx'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; import {ValueOf} from 'type-fest'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; @@ -22,6 +22,9 @@ import Text from './Text'; type AvatarWithDisplayNamePropsWithOnyx = { /** All of the actions of the report */ parentReportActions: OnyxEntry; + + /** Personal details of all users */ + personalDetails: OnyxEntry>; }; type AvatarWithDisplayNameProps = AvatarWithDisplayNamePropsWithOnyx & { @@ -34,9 +37,6 @@ type AvatarWithDisplayNameProps = AvatarWithDisplayNamePropsWithOnyx & { /** The size of the avatar */ size?: ValueOf; - /** Personal details of all the users */ - personalDetails: OnyxCollection; - /** Whether if it's an unauthenticated user */ isAnonymous?: boolean; @@ -45,13 +45,13 @@ type AvatarWithDisplayNameProps = AvatarWithDisplayNamePropsWithOnyx & { }; function AvatarWithDisplayName({ - personalDetails, policy, report, parentReportActions, isAnonymous = false, size = CONST.AVATAR_SIZE.DEFAULT, shouldEnableDetailPageNavigation = false, + personalDetails = CONST.EMPTY_OBJECT, }: AvatarWithDisplayNameProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -179,4 +179,7 @@ export default withOnyx `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '0'}`, canEvict: false, }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, })(AvatarWithDisplayName); diff --git a/src/components/LHNOptionsList/LHNOptionsList.js b/src/components/LHNOptionsList/LHNOptionsList.js index 7329b981010c..b2fc25d63c66 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.js +++ b/src/components/LHNOptionsList/LHNOptionsList.js @@ -5,6 +5,7 @@ import React, {memo, useCallback} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; +import participantPropTypes from '@components/participantPropTypes'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import {getReportActionsByReportID} from '@libs/ReportActionsUtils'; import {getReportByID} from '@libs/ReportUtils'; @@ -47,6 +48,9 @@ const propTypes = { /** Indicates which locale the user currently has selected */ preferredLocale: PropTypes.string, + + /** List of users' personal details */ + personalDetails: PropTypes.objectOf(participantPropTypes), }; const defaultProps = { @@ -54,11 +58,12 @@ const defaultProps = { shouldDisableFocusOptions: false, policy: {}, preferredLocale: CONST.LOCALES.DEFAULT, + personalDetails: {}, }; const keyExtractor = (item) => `report_${item}`; -function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optionMode, shouldDisableFocusOptions, preferredLocale}) { +function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optionMode, shouldDisableFocusOptions, preferredLocale, personalDetails}) { const styles = useThemeStyles(); /** * Function which renders a row in the list @@ -77,7 +82,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const transactionID = lodashGet(itemParentReportAction, ['originalMessage', 'IOUTransactionID'], ''); const participants = [...ReportUtils.getParticipantsIDs(itemFullReport), itemFullReport.ownerAccountID]; - const participantsPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants); + const participantsPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants, personalDetails); return ( ); }, - [onSelectRow, optionMode, preferredLocale, shouldDisableFocusOptions], + [onSelectRow, optionMode, personalDetails, preferredLocale, shouldDisableFocusOptions], ); return ( @@ -122,4 +127,7 @@ export default withOnyx({ preferredLocale: { key: ONYXKEYS.NVP_PREFERRED_LOCALE, }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, })(memo(LHNOptionsList)); diff --git a/src/components/LHNOptionsList/OptionRowLHNData.js b/src/components/LHNOptionsList/OptionRowLHNData.js index 1e3d25ab7b3b..d499f2401cc9 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.js +++ b/src/components/LHNOptionsList/OptionRowLHNData.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import React, {useMemo, useRef} from 'react'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; +import participantPropTypes from '@components/participantPropTypes'; import transactionPropTypes from '@components/transactionPropTypes'; import useCurrentReportID from '@hooks/useCurrentReportID'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; @@ -17,6 +18,9 @@ const propTypes = { /** Whether row should be focused */ isFocused: PropTypes.bool, + /** List of users' personal details */ + personalDetails: PropTypes.objectOf(participantPropTypes), + /** The preferred language for the app */ preferredLocale: PropTypes.string, @@ -45,6 +49,7 @@ const propTypes = { const defaultProps = { isFocused: false, + personalDetails: {}, fullReport: {}, policy: {}, parentReportAction: {}, @@ -59,7 +64,7 @@ const defaultProps = { * The OptionRowLHN component is memoized, so it will only * re-render if the data really changed. */ -function OptionRowLHNData({isFocused, fullReport, reportActions, preferredLocale, comment, policy, parentReportAction, transaction, ...propsToForward}) { +function OptionRowLHNData({isFocused, fullReport, reportActions, personalDetails, preferredLocale, comment, policy, parentReportAction, transaction, ...propsToForward}) { const reportID = propsToForward.reportID; const {currentReportID} = useCurrentReportID(); const isReportFocused = isFocused && currentReportID === reportID; @@ -74,7 +79,7 @@ function OptionRowLHNData({isFocused, fullReport, reportActions, preferredLocale const optionItem = useMemo(() => { // Note: ideally we'd have this as a dependent selector in onyx! - const item = SidebarUtils.getOptionData(fullReport, reportActions, preferredLocale, policy, parentReportAction); + const item = SidebarUtils.getOptionData(fullReport, reportActions, personalDetails, preferredLocale, policy, parentReportAction); if (deepEqual(item, optionItemRef.current)) { return optionItemRef.current; } diff --git a/src/components/MoneyReportHeader.js b/src/components/MoneyReportHeader.js index c5fab09e1711..9cb9d392004d 100644 --- a/src/components/MoneyReportHeader.js +++ b/src/components/MoneyReportHeader.js @@ -20,7 +20,7 @@ import ROUTES from '@src/ROUTES'; import Button from './Button'; import HeaderWithBackButton from './HeaderWithBackButton'; import MoneyReportHeaderStatusBar from './MoneyReportHeaderStatusBar'; -import participantPropTypes from './participantPropTypes'; +import {usePersonalDetails} from './OnyxProvider'; import SettlementButton from './SettlementButton'; import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions'; @@ -46,9 +46,6 @@ const propTypes = { /** The next step for the report */ nextStep: nextStepPropTypes, - /** Personal details so we can get the ones for the report participants */ - personalDetails: PropTypes.objectOf(participantPropTypes).isRequired, - /** Session info for the currently logged in user. */ session: PropTypes.shape({ /** Currently logged in user email */ @@ -67,7 +64,8 @@ const defaultProps = { policy: {}, }; -function MoneyReportHeader({session, personalDetails, policy, chatReport, nextStep, report: moneyRequestReport, isSmallScreenWidth}) { +function MoneyReportHeader({session, policy, chatReport, nextStep, report: moneyRequestReport, isSmallScreenWidth}) { + const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; const styles = useThemeStyles(); const {translate} = useLocalize(); const reimbursableTotal = ReportUtils.getMoneyRequestReimbursableTotal(moneyRequestReport); @@ -211,8 +209,5 @@ export default compose( session: { key: ONYXKEYS.SESSION, }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, }), )(MoneyReportHeader); diff --git a/src/components/MoneyRequestHeader.js b/src/components/MoneyRequestHeader.js index 51e8143a5c76..db8ab3c3a3eb 100644 --- a/src/components/MoneyRequestHeader.js +++ b/src/components/MoneyRequestHeader.js @@ -22,7 +22,7 @@ import ConfirmModal from './ConfirmModal'; import HeaderWithBackButton from './HeaderWithBackButton'; import * as Expensicons from './Icon/Expensicons'; import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar'; -import participantPropTypes from './participantPropTypes'; +import {usePersonalDetails} from './OnyxProvider'; import transactionPropTypes from './transactionPropTypes'; const propTypes = { @@ -35,9 +35,6 @@ const propTypes = { name: PropTypes.string, }), - /** Personal details so we can get the ones for the report participants */ - personalDetails: PropTypes.objectOf(participantPropTypes).isRequired, - /* Onyx Props */ /** Session info for the currently logged in user. */ session: PropTypes.shape({ @@ -65,7 +62,8 @@ const defaultProps = { policy: {}, }; -function MoneyRequestHeader({session, parentReport, report, parentReportAction, transaction, policy, personalDetails}) { +function MoneyRequestHeader({session, parentReport, report, parentReportAction, transaction, policy}) { + const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; const styles = useThemeStyles(); const {translate} = useLocalize(); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); @@ -174,9 +172,6 @@ export default compose( key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '0'}`, canEvict: false, }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, }), // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file withOnyx({ diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 71ab6e407712..bace29e06d28 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -1,6 +1,6 @@ /* eslint-disable rulesdir/prefer-underscore-method */ import Str from 'expensify-common/lib/str'; -import Onyx, {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import Onyx, {OnyxCollection} from 'react-native-onyx'; import {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -47,12 +47,6 @@ Onyx.connect({ }, }); -let allPersonalDetails: OnyxEntry>; -Onyx.connect({ - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - callback: (val) => (allPersonalDetails = val), -}); - // Session can remain stale because the only way for the current user to change is to // sign out and sign in, which would clear out all the Onyx // data anyway and cause SidebarLinks to rerender. @@ -239,6 +233,7 @@ type ActorDetails = { function getOptionData( report: Report, reportActions: Record, + personalDetails: Record, preferredLocale: ValueOf, policy: Policy, parentReportAction: ReportAction, @@ -246,7 +241,6 @@ function getOptionData( // When a user signs out, Onyx is cleared. Due to the lazy rendering with a virtual list, it's possible for // this method to be called after the Onyx data has been cleared out. In that case, it's fine to do // a null check here and return early. - const personalDetails = allPersonalDetails; if (!report || !personalDetails) { return; } diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index 595f6ad1bbe3..7f9957232cfb 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -3,10 +3,12 @@ import {measureFunction} from 'reassure'; import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {PersonalDetails} from '@src/types/onyx'; import Policy from '@src/types/onyx/Policy'; import Report from '@src/types/onyx/Report'; import ReportAction, {ReportActions} from '@src/types/onyx/ReportAction'; import createCollection from '../utils/collections/createCollection'; +import createPersonalDetails from '../utils/collections/personalDetails'; import createRandomPolicy from '../utils/collections/policies'; import createRandomReportAction from '../utils/collections/reportActions'; import createRandomReport from '../utils/collections/reports'; @@ -36,6 +38,11 @@ const reportActions = createCollection( (index) => createRandomReportAction(index), ); +const personalDetails = createCollection( + (item) => item.accountID, + (index) => createPersonalDetails(index), +); + const mockedResponseMap = getMockedReports(5000) as Record<`${typeof ONYXKEYS.COLLECTION.REPORT}`, Report>; const runs = CONST.PERFORMANCE_TESTS.RUNS; @@ -50,7 +57,7 @@ test('getOptionData on 5k reports', async () => { }); await waitForBatchedUpdates(); - await measureFunction(() => SidebarUtils.getOptionData(report, reportActions, preferredLocale, policy, parentReportAction), {runs}); + await measureFunction(() => SidebarUtils.getOptionData(report, reportActions, personalDetails, preferredLocale, policy, parentReportAction), {runs}); }); test('getOrderedReportIDs on 5k reports', async () => { From c968ccfe8613e473400a4a3d579c2d38db5ae21e Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 8 Dec 2023 17:53:04 +0500 Subject: [PATCH 011/583] revert: bring back old improvements for LHN --- .../LHNOptionsList/LHNOptionsList.js | 91 +++++++++++++++---- src/components/LHNOptionsList/OptionRowLHN.js | 5 +- .../LHNOptionsList/OptionRowLHNData.js | 36 ++++---- 3 files changed, 92 insertions(+), 40 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.js b/src/components/LHNOptionsList/LHNOptionsList.js index b2fc25d63c66..59b1abd666c6 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.js +++ b/src/components/LHNOptionsList/LHNOptionsList.js @@ -1,15 +1,16 @@ import {FlashList} from '@shopify/flash-list'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {memo, useCallback} from 'react'; +import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import participantPropTypes from '@components/participantPropTypes'; +import transactionPropTypes from '@components/transactionPropTypes'; import * as OptionsListUtils from '@libs/OptionsListUtils'; -import {getReportActionsByReportID} from '@libs/ReportActionsUtils'; -import {getReportByID} from '@libs/ReportUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; +import reportPropTypes from '@pages/reportPropTypes'; import stylePropTypes from '@styles/stylePropTypes'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; @@ -46,24 +47,53 @@ const propTypes = { avatar: PropTypes.string, }), + /** All reports shared with the user */ + reports: PropTypes.objectOf(reportPropTypes), + + /** Array of report actions for this report */ + reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), + /** Indicates which locale the user currently has selected */ preferredLocale: PropTypes.string, /** List of users' personal details */ personalDetails: PropTypes.objectOf(participantPropTypes), + + /** The transaction from the parent report action */ + transactions: PropTypes.objectOf(transactionPropTypes), + /** List of draft comments */ + draftComments: PropTypes.objectOf(PropTypes.string), }; const defaultProps = { style: undefined, shouldDisableFocusOptions: false, + reportActions: {}, + reports: {}, policy: {}, preferredLocale: CONST.LOCALES.DEFAULT, personalDetails: {}, + transactions: {}, + draftComments: {}, }; const keyExtractor = (item) => `report_${item}`; -function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optionMode, shouldDisableFocusOptions, preferredLocale, personalDetails}) { +function LHNOptionsList({ + style, + contentContainerStyles, + data, + onSelectRow, + optionMode, + shouldDisableFocusOptions, + reports, + reportActions, + policy, + preferredLocale, + personalDetails, + transactions, + draftComments, +}) { const styles = useThemeStyles(); /** * Function which renders a row in the list @@ -75,11 +105,14 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio */ const renderItem = useCallback( ({item: reportID}) => { - const itemFullReport = getReportByID(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); - const itemReportActions = getReportActionsByReportID(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`); - const itemParentReportActions = getReportActionsByReportID(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport.parentReportID}`); + const itemFullReport = reports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] || {}; + const itemReportActions = reportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]; + const itemParentReportActions = reportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport.parentReportID}`] || {}; const itemParentReportAction = itemParentReportActions[itemFullReport.parentReportActionID] || {}; + const itemPolicy = policy[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport.policyID}`] || {}; const transactionID = lodashGet(itemParentReportAction, ['originalMessage', 'IOUTransactionID'], ''); + const itemTransaction = transactionID ? transactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] : {}; + const itemComment = draftComments[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] || ''; const participants = [...ReportUtils.getParticipantsIDs(itemFullReport), itemFullReport.ownerAccountID]; const participantsPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants, personalDetails); @@ -90,18 +123,27 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio fullReport={itemFullReport} reportActions={itemReportActions} parentReportAction={itemParentReportAction} + policy={itemPolicy} personalDetails={participantsPersonalDetails} - transactionID={transactionID} + transaction={itemTransaction} + receiptTransactions={transactions} viewMode={optionMode} isFocused={!shouldDisableFocusOptions} onSelectRow={onSelectRow} preferredLocale={preferredLocale} + comment={itemComment} /> ); }, - [onSelectRow, optionMode, personalDetails, preferredLocale, shouldDisableFocusOptions], + [draftComments, onSelectRow, optionMode, personalDetails, policy, preferredLocale, reportActions, reports, shouldDisableFocusOptions, transactions], ); + const extraData = useMemo(() => [ + reportActions, + policy, + personalDetails, + ], [reportActions, policy, personalDetails]) + return ( ); @@ -124,10 +167,26 @@ LHNOptionsList.defaultProps = defaultProps; LHNOptionsList.displayName = 'LHNOptionsList'; export default withOnyx({ - preferredLocale: { - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, -})(memo(LHNOptionsList)); + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, + reportActions: { + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + }, + policy: { + key: ONYXKEYS.COLLECTION.POLICY, + }, + preferredLocale: { + key: ONYXKEYS.NVP_PREFERRED_LOCALE, + }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + transactions: { + key: ONYXKEYS.COLLECTION.TRANSACTION, + }, + draftComments: { + key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, + }, + } +)(LHNOptionsList); diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index 0aa6412eefab..3b2de574ba17 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -52,8 +52,6 @@ const propTypes = { /** The item that should be rendered */ // eslint-disable-next-line react/forbid-prop-types optionItem: PropTypes.object, - - hasDraft: PropTypes.bool, }; const defaultProps = { @@ -63,7 +61,6 @@ const defaultProps = { style: null, optionItem: null, isFocused: false, - hasDraft: false, }; function OptionRowLHN(props) { @@ -295,7 +292,7 @@ function OptionRowLHN(props) { /> )} - {props.hasDraft && optionItem.isAllowedToComment && ( + {optionItem.hasDraftComment && optionItem.isAllowedToComment && ( { // Note: ideally we'd have this as a dependent selector in onyx! @@ -88,7 +98,7 @@ function OptionRowLHNData({isFocused, fullReport, reportActions, personalDetails // Listen parentReportAction to update title of thread report when parentReportAction changed // Listen to transaction to update title of transaction report when transaction changed // eslint-disable-next-line react-hooks/exhaustive-deps - }, [fullReport, linkedTransaction, reportActions, preferredLocale, policy, parentReportAction, transaction]); + }, [fullReport, linkedTransaction, reportActions, personalDetails, preferredLocale, policy, parentReportAction, transaction]); return ( ); } @@ -112,17 +121,4 @@ OptionRowLHNData.displayName = 'OptionRowLHNData'; * Thats also why the React.memo is used on the outer component here, as we just * use it to prevent re-renders from parent re-renders. */ -export default withOnyx({ - policy: { - key: ({fullReport}) => `${ONYXKEYS.COLLECTION.POLICY}${fullReport.policyID}`, - initialValue: {}, - }, - comment: { - key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, - initialValue: '', - }, - transaction: { - key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - initialValue: {}, - }, -})(React.memo(OptionRowLHNData)); +export default React.memo(OptionRowLHNData); From 697556664023327e2539ac51b80217ee55684b6f Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 8 Dec 2023 17:55:00 +0500 Subject: [PATCH 012/583] fix: personalDetails in HeaderView --- src/pages/home/HeaderView.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index 5b57419c8530..2b6fa35508db 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -311,5 +311,8 @@ export default memo( key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, selector: (policy) => _.pick(policy, ['name', 'avatar', 'pendingAction']), }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, })(HeaderView), ); From b7fa91674cb5c2dbe4b19e81b907f8eacd766c99 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 8 Dec 2023 17:58:41 +0500 Subject: [PATCH 013/583] refactor: remove unused code --- src/libs/ReportActionsUtils.ts | 5 ----- src/libs/ReportUtils.ts | 5 ----- 2 files changed, 10 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 1c8d1ef06dfd..f58021e17064 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -518,10 +518,6 @@ function getReportAction(reportID: string, reportActionID: string): OnyxEntry { - return allReportActions?.[reportID] ?? {}; -} - function getMostRecentReportActionLastModified(): string { // Start with the oldest date possible let mostRecentReportActionLastModified = new Date(0).toISOString(); @@ -677,7 +673,6 @@ export { getNumberOfMoneyRequests, getParentReportAction, getReportAction, - getReportActionsByReportID, getReportPreviewAction, getSortedReportActions, getSortedReportActionsForDisplay, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a45a20d7b45f..23382e87936a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4284,12 +4284,7 @@ function navigateToPrivateNotes(report: Report, session: Session) { Navigation.navigate(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID)); } -function getReportByID(reportID: string) { - return allReports ? allReports[reportID] : {}; -} - export { - getReportByID, getReportParticipantsTitle, isReportMessageAttachment, findLastAccessedReport, From 027af7f88da6c9669d4d44d637c5be1adabbf129 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 11 Dec 2023 13:58:51 +0500 Subject: [PATCH 014/583] fix: linting --- .../LHNOptionsList/LHNOptionsList.js | 51 +++++++++---------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.js b/src/components/LHNOptionsList/LHNOptionsList.js index 59b1abd666c6..8efca88e04d7 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.js +++ b/src/components/LHNOptionsList/LHNOptionsList.js @@ -138,11 +138,7 @@ function LHNOptionsList({ [draftComments, onSelectRow, optionMode, personalDetails, policy, preferredLocale, reportActions, reports, shouldDisableFocusOptions, transactions], ); - const extraData = useMemo(() => [ - reportActions, - policy, - personalDetails, - ], [reportActions, policy, personalDetails]) + const extraData = useMemo(() => [reportActions, policy, personalDetails], [reportActions, policy, personalDetails]); return ( @@ -167,26 +163,25 @@ LHNOptionsList.defaultProps = defaultProps; LHNOptionsList.displayName = 'LHNOptionsList'; export default withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, - reportActions: { - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - }, - policy: { - key: ONYXKEYS.COLLECTION.POLICY, - }, - preferredLocale: { - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - transactions: { - key: ONYXKEYS.COLLECTION.TRANSACTION, - }, - draftComments: { - key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, - }, - } -)(LHNOptionsList); + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, + reportActions: { + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + }, + policy: { + key: ONYXKEYS.COLLECTION.POLICY, + }, + preferredLocale: { + key: ONYXKEYS.NVP_PREFERRED_LOCALE, + }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + transactions: { + key: ONYXKEYS.COLLECTION.TRANSACTION, + }, + draftComments: { + key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, + }, +})(LHNOptionsList); From 200a7bdaec975d50c80c3916bf94345f4b2bcf16 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 11 Dec 2023 14:16:35 +0500 Subject: [PATCH 015/583] fix: add reports to extraData in LHNOptionsList --- src/components/LHNOptionsList/LHNOptionsList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.js b/src/components/LHNOptionsList/LHNOptionsList.js index 8efca88e04d7..699d3f9dff33 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.js +++ b/src/components/LHNOptionsList/LHNOptionsList.js @@ -138,7 +138,7 @@ function LHNOptionsList({ [draftComments, onSelectRow, optionMode, personalDetails, policy, preferredLocale, reportActions, reports, shouldDisableFocusOptions, transactions], ); - const extraData = useMemo(() => [reportActions, policy, personalDetails], [reportActions, policy, personalDetails]); + const extraData = useMemo(() => [reportActions, reports, policy, personalDetails], [reportActions, reports, policy, personalDetails]); return ( From e37e16d36c482466a073f8a9436c8d7cef37729e Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 14 Dec 2023 14:47:17 +0500 Subject: [PATCH 016/583] fix: skeleton not showing and screen transition on mWeb --- src/pages/home/ReportScreen.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 55cbd684b828..47fad4a5e849 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -62,7 +62,7 @@ const propTypes = { reportMetadata: reportMetadataPropTypes, /** Array of report actions for this report */ - reportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)), + reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), /** Whether the composer is full size */ isComposerFullSize: PropTypes.bool, @@ -190,6 +190,7 @@ function ReportScreen({ const isTopMostReportId = currentReportID === getReportID(route); const didSubscribeToReportLeavingEvents = useRef(false); + const [hasTransitioned, setHasTransitioned] = useState(false); const goBack = useCallback(() => { Navigation.goBack(ROUTES.HOME, false, true); @@ -388,6 +389,10 @@ function ReportScreen({ [report, reportMetadata, isLoading, shouldHideReport, isOptimisticDelete, userLeavingStatus], ); + const onEntryTransitionEnd = useCallback(() => { + setHasTransitioned(true); + }, []); + const actionListValue = useMemo(() => ({flatListRef, scrollPosition, setScrollPosition}), [flatListRef, scrollPosition, setScrollPosition]); return ( @@ -397,6 +402,7 @@ function ReportScreen({ style={screenWrapperStyle} shouldEnableKeyboardAvoidingView={isTopMostReportId} testID={ReportScreen.displayName} + onEntryTransitionEnd={onEntryTransitionEnd} > - {isReportReadyForDisplay && !isLoadingInitialReportActions && !isLoading && ( + {isReportReadyForDisplay && hasTransitioned && !isLoadingInitialReportActions && !isLoading && ( } + {(!isReportReadyForDisplay || !hasTransitioned || isLoadingInitialReportActions || isLoading) && } - {isReportReadyForDisplay ? ( + {isReportReadyForDisplay && hasTransitioned ? ( - ) : ( - - )} + ) : null} From fcf15a372c646dd431d613ded3d9d5beb67af19e Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 19 Dec 2023 14:31:17 +0500 Subject: [PATCH 017/583] refactor: use didScreenTransitionEnd --- src/pages/home/ReportScreen.js | 146 ++++++++++++++++----------------- 1 file changed, 72 insertions(+), 74 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 50c8cea57056..f513964c0948 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -199,7 +199,6 @@ function ReportScreen({ const isTopMostReportId = currentReportID === getReportID(route); const didSubscribeToReportLeavingEvents = useRef(false); - const [hasTransitioned, setHasTransitioned] = useState(false); const goBack = useCallback(() => { Navigation.goBack(ROUTES.HOME, false, true); @@ -313,7 +312,9 @@ function ReportScreen({ ComposerActions.setShouldShowComposeInput(true); }); return () => { - interactionTask.cancel(); + if (interactionTask) { + interactionTask.cancel(); + } if (!didSubscribeToReportLeavingEvents) { return; } @@ -413,10 +414,6 @@ function ReportScreen({ [report, reportMetadata, isLoading, shouldHideReport, isOptimisticDelete, userLeavingStatus], ); - const onEntryTransitionEnd = useCallback(() => { - setHasTransitioned(true); - }, []); - const actionListValue = useMemo(() => ({flatListRef, scrollPosition, setScrollPosition}), [flatListRef, scrollPosition, setScrollPosition]); return ( @@ -426,79 +423,80 @@ function ReportScreen({ style={screenWrapperStyle} shouldEnableKeyboardAvoidingView={isTopMostReportId} testID={ReportScreen.displayName} - onEntryTransitionEnd={onEntryTransitionEnd} > - - ( + - {headerView} - {ReportUtils.isTaskReport(report) && isSmallScreenWidth && ReportUtils.isOpenTaskReport(report, parentReportAction) && ( - - - - + + {headerView} + {ReportUtils.isTaskReport(report) && isSmallScreenWidth && ReportUtils.isOpenTaskReport(report, parentReportAction) && ( + + + + + - - )} - - {!!accountManagerReportID && ReportUtils.isConciergeChatReport(report) && isBannerVisible && ( - - )} - - - {isReportReadyForDisplay && hasTransitioned && !isLoadingInitialReportActions && !isLoading && ( - )} - - {/* Note: The ReportActionsSkeletonView should be allowed to mount even if the initial report actions are not loaded. - If we prevent rendering the report while they are loading then - we'll unnecessarily unmount the ReportActionsView which will clear the new marker lines initial state. */} - {(!isReportReadyForDisplay || !hasTransitioned || isLoadingInitialReportActions || isLoading) && } - - {isReportReadyForDisplay && hasTransitioned ? ( - - ) : null} - - - + + {!!accountManagerReportID && ReportUtils.isConciergeChatReport(report) && isBannerVisible && ( + + )} + + + {isReportReadyForDisplay && didScreenTransitionEnd && !isLoadingInitialReportActions && !isLoading && ( + + )} + + {/* Note: The ReportActionsSkeletonView should be allowed to mount even if the initial report actions are not loaded. + If we prevent rendering the report while they are loading then + we'll unnecessarily unmount the ReportActionsView which will clear the new marker lines initial state. */} + {(!isReportReadyForDisplay || !didScreenTransitionEnd || isLoadingInitialReportActions || isLoading) && } + + {isReportReadyForDisplay && didScreenTransitionEnd ? ( + + ) : null} + + + + )} From e4682e904a31e73610b6162fb74d1206d9f433b5 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 19 Dec 2023 14:38:41 +0500 Subject: [PATCH 018/583] fix: typecheck in AvatarWithDisplayName --- src/components/AvatarWithDisplayName.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index a409da9ea068..24beb7f1f16d 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -11,7 +11,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {PersonalDetails, Policy, Report, ReportActions} from '@src/types/onyx'; +import {PersonalDetailsList, Policy, Report, ReportActions} from '@src/types/onyx'; import DisplayNames from './DisplayNames'; import MultipleAvatars from './MultipleAvatars'; import ParentNavigationSubtitle from './ParentNavigationSubtitle'; @@ -24,7 +24,7 @@ type AvatarWithDisplayNamePropsWithOnyx = { parentReportActions: OnyxEntry; /** Personal details of all users */ - personalDetails: OnyxEntry>; + personalDetails: OnyxEntry; }; type AvatarWithDisplayNameProps = AvatarWithDisplayNamePropsWithOnyx & { From eb7eaa6b982f9c08df0cf555b427027ec50b1133 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 3 Jan 2024 15:32:11 +0500 Subject: [PATCH 019/583] refactor: remove not needed personal details The reason is that, we only use personalDetails to pass to AvatarWithDisplayName and now this component already have Onyx.connect to provide personalDetails directly --- src/components/HeaderWithBackButton/index.tsx | 2 -- src/components/HeaderWithBackButton/types.ts | 7 ++----- src/components/MoneyReportHeader.js | 3 --- src/components/MoneyRequestHeader.js | 3 --- 4 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index 9ec8bca55a95..44964370fab3 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -30,7 +30,6 @@ function HeaderWithBackButton({ onThreeDotsButtonPress = () => {}, report = null, policy, - personalDetails = null, shouldShowAvatarWithDisplay = false, shouldShowBackButton = true, shouldShowBorderBottom = false, @@ -103,7 +102,6 @@ function HeaderWithBackButton({ ) : ( diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts index 99e93e8d18d2..81ee0ab6a0b1 100644 --- a/src/components/HeaderWithBackButton/types.ts +++ b/src/components/HeaderWithBackButton/types.ts @@ -1,9 +1,9 @@ import {ReactNode} from 'react'; -import {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import {OnyxEntry} from 'react-native-onyx'; import type {Action} from '@hooks/useSingleExecution'; import type {StepCounterParams} from '@src/languages/types'; import type {AnchorPosition} from '@src/styles'; -import type {PersonalDetails, Policy, Report} from '@src/types/onyx'; +import type {Policy, Report} from '@src/types/onyx'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -91,9 +91,6 @@ type HeaderWithBackButtonProps = Partial & { /** The report's policy, if we're showing the details for a report and need info about it for AvatarWithDisplay */ policy?: OnyxEntry; - /** Policies, if we're showing the details for a report and need participant details for AvatarWithDisplay */ - personalDetails?: OnyxCollection; - /** Single execution function to prevent concurrent navigation actions */ singleExecution?: (action: Action) => Action; diff --git a/src/components/MoneyReportHeader.js b/src/components/MoneyReportHeader.js index 2731d3d76254..bd3b6d28bb44 100644 --- a/src/components/MoneyReportHeader.js +++ b/src/components/MoneyReportHeader.js @@ -26,7 +26,6 @@ import ROUTES from '@src/ROUTES'; import Button from './Button'; import HeaderWithBackButton from './HeaderWithBackButton'; import MoneyReportHeaderStatusBar from './MoneyReportHeaderStatusBar'; -import {usePersonalDetails} from './OnyxProvider'; import SettlementButton from './SettlementButton'; import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions'; @@ -76,7 +75,6 @@ const defaultProps = { }; function MoneyReportHeader({session, policy, chatReport, nextStep, report: moneyRequestReport, isSmallScreenWidth}) { - const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; const styles = useThemeStyles(); const {translate} = useLocalize(); const {windowWidth} = useWindowDimensions(); @@ -143,7 +141,6 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money shouldShowPinButton={false} report={moneyRequestReport} policy={policy} - personalDetails={personalDetails} shouldShowBackButton={isSmallScreenWidth} onBackButtonPress={() => Navigation.goBack(ROUTES.HOME, false, true)} // Shows border if no buttons or next steps are showing below the header diff --git a/src/components/MoneyRequestHeader.js b/src/components/MoneyRequestHeader.js index 97091230b2b6..6236e13990ed 100644 --- a/src/components/MoneyRequestHeader.js +++ b/src/components/MoneyRequestHeader.js @@ -22,7 +22,6 @@ import ConfirmModal from './ConfirmModal'; import HeaderWithBackButton from './HeaderWithBackButton'; import * as Expensicons from './Icon/Expensicons'; import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar'; -import {usePersonalDetails} from './OnyxProvider'; import transactionPropTypes from './transactionPropTypes'; const propTypes = { @@ -63,7 +62,6 @@ const defaultProps = { }; function MoneyRequestHeader({session, parentReport, report, parentReportAction, transaction, policy}) { - const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; const styles = useThemeStyles(); const {translate} = useLocalize(); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); @@ -123,7 +121,6 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, ownerAccountID: lodashGet(parentReport, 'ownerAccountID', null), }} policy={policy} - personalDetails={personalDetails} shouldShowBackButton={isSmallScreenWidth} onBackButtonPress={() => Navigation.goBack(ROUTES.HOME, false, true)} /> From 8b71ff6c3deac917899a632581918b3858d22d94 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 8 Jan 2024 14:27:40 +0500 Subject: [PATCH 020/583] test: fix onEntryTransition not being called in UnitTests --- src/pages/home/ReportScreen.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index e4291d7900bd..5fa21da71005 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -202,6 +202,7 @@ function ReportScreen({ const isTopMostReportId = currentReportID === getReportID(route); const didSubscribeToReportLeavingEvents = useRef(false); + const [didScreenTransitionEnd, setEntryTransitionEnd] = useState(false); useEffect(() => { if (!report || !report.reportID || shouldHideReport) { @@ -429,6 +430,18 @@ function ReportScreen({ }; }, [report, didSubscribeToReportLeavingEvents, reportID]); + useEffect(() => { + const interactionTask = InteractionManager.runAfterInteractions(() => { + setEntryTransitionEnd(true); + }); + return () => { + if (!interactionTask) { + return; + } + interactionTask.cancel(); + }; + }, []); + const onListLayout = useCallback((e) => { setListHeight((prev) => lodashGet(e, 'nativeEvent.layout.height', prev)); if (!markReadyForHydration) { @@ -464,7 +477,7 @@ function ReportScreen({ shouldEnableKeyboardAvoidingView={isTopMostReportId} testID={ReportScreen.displayName} > - {({didScreenTransitionEnd}) => ( + {() => ( Date: Fri, 12 Jan 2024 08:56:46 +0100 Subject: [PATCH 021/583] ref: wip --- src/libs/ReportUtils.ts | 2 +- .../focusTextInputAfterAnimation/index.ts | 2 +- .../focusTextInputAfterAnimation/types.ts | 2 +- src/libs/isReportMessageAttachment.ts | 10 +- ...portActionItem.js => ReportActionItem.tsx} | 487 +++++++++--------- src/types/onyx/OriginalMessage.ts | 9 +- 6 files changed, 255 insertions(+), 257 deletions(-) rename src/pages/home/report/{ReportActionItem.js => ReportActionItem.tsx} (62%) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e619cb3c80dd..f33387a1e0f0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3559,7 +3559,7 @@ function getAllPolicyReports(policyID: string): Array> { /** * Returns true if Chronos is one of the chat participants (1:1) */ -function chatIncludesChronos(report: OnyxEntry): boolean { +function chatIncludesChronos(report: OnyxEntry | EmptyObject): boolean { return Boolean(report?.participantAccountIDs?.includes(CONST.ACCOUNT_ID.CHRONOS)); } diff --git a/src/libs/focusTextInputAfterAnimation/index.ts b/src/libs/focusTextInputAfterAnimation/index.ts index 3f7c6555b5ce..66d0c35c1a63 100644 --- a/src/libs/focusTextInputAfterAnimation/index.ts +++ b/src/libs/focusTextInputAfterAnimation/index.ts @@ -4,7 +4,7 @@ import type FocusTextInputAfterAnimation from './types'; * This library is a no-op for all platforms except for Android and iOS and will immediately focus the given input without any delays. */ const focusTextInputAfterAnimation: FocusTextInputAfterAnimation = (inputRef) => { - inputRef.focus(); + inputRef?.focus(); }; export default focusTextInputAfterAnimation; diff --git a/src/libs/focusTextInputAfterAnimation/types.ts b/src/libs/focusTextInputAfterAnimation/types.ts index a6a14165598b..bfe29317c1ef 100644 --- a/src/libs/focusTextInputAfterAnimation/types.ts +++ b/src/libs/focusTextInputAfterAnimation/types.ts @@ -1,5 +1,5 @@ import type {TextInput} from 'react-native'; -type FocusTextInputAfterAnimation = (inputRef: TextInput | HTMLInputElement, animationLength: number) => void; +type FocusTextInputAfterAnimation = (inputRef: TextInput | HTMLInputElement | undefined, animationLength: number) => void; export default FocusTextInputAfterAnimation; diff --git a/src/libs/isReportMessageAttachment.ts b/src/libs/isReportMessageAttachment.ts index df8a589f7bdc..5ff4bfbef093 100644 --- a/src/libs/isReportMessageAttachment.ts +++ b/src/libs/isReportMessageAttachment.ts @@ -7,15 +7,15 @@ import type {Message} from '@src/types/onyx/ReportAction'; * * @param reportActionMessage report action's message as text, html and translationKey */ -export default function isReportMessageAttachment({text, html, translationKey}: Message): boolean { - if (!text || !html) { +export default function isReportMessageAttachment(message: Message | undefined): boolean { + if (!message?.text || !message?.html) { return false; } - if (translationKey && text === CONST.ATTACHMENT_MESSAGE_TEXT) { - return translationKey === CONST.TRANSLATION_KEYS.ATTACHMENT; + if (message?.translationKey && message?.text === CONST.ATTACHMENT_MESSAGE_TEXT) { + return message?.translationKey === CONST.TRANSLATION_KEYS.ATTACHMENT; } const regex = new RegExp(` ${CONST.ATTACHMENT_SOURCE_ATTRIBUTE}="(.*)"`, 'i'); - return text === CONST.ATTACHMENT_MESSAGE_TEXT && (!!html.match(regex) || html === CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML); + return message?.text === CONST.ATTACHMENT_MESSAGE_TEXT && (!!message?.html.match(regex) || message?.html === CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML); } diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.tsx similarity index 62% rename from src/pages/home/report/ReportActionItem.js rename to src/pages/home/report/ReportActionItem.tsx index b1130af5d2ff..8e42612c260e 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.tsx @@ -1,9 +1,7 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {memo, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager, 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 DisplayNames from '@components/DisplayNames'; import Hoverable from '@components/Hoverable'; @@ -14,7 +12,6 @@ import KYCWall from '@components/KYCWall'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {usePersonalDetails, withBlockedFromConcierge, withNetwork, withReportActionsDrafts} from '@components/OnyxProvider'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; -import EmojiReactionsPropTypes from '@components/Reactions/EmojiReactionsPropTypes'; import ReportActionItemEmojiReactions from '@components/Reactions/ReportActionItemEmojiReactions'; import RenderHTML from '@components/RenderHTML'; import ChronosOOOListActions from '@components/ReportActionItem/ChronosOOOListActions'; @@ -29,12 +26,12 @@ import TaskView from '@components/ReportActionItem/TaskView'; import {ShowContextMenuContext} from '@components/ShowContextMenuContext'; import Text from '@components/Text'; import UnreadActionIndicator from '@components/UnreadActionIndicator'; -import withLocalize from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; +import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import ControlSelection from '@libs/ControlSelection'; import * as CurrencyUtils from '@libs/CurrencyUtils'; @@ -47,9 +44,7 @@ import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import SelectionScraper from '@libs/SelectionScraper'; -import userWalletPropTypes from '@pages/EnablePayments/userWalletPropTypes'; import {ReactionListContext} from '@pages/home/ReportScreenContext'; -import reportPropTypes from '@pages/reportPropTypes'; import * as BankAccounts from '@userActions/BankAccounts'; import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; import * as store from '@userActions/ReimbursementAccount/store'; @@ -60,6 +55,9 @@ import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {DecisionName} from '@src/types/onyx/OriginalMessage'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; import MiniReportActionContextMenu from './ContextMenu/MiniReportActionContextMenu'; import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu'; @@ -73,188 +71,195 @@ import ReportActionItemMessage from './ReportActionItemMessage'; import ReportActionItemMessageEdit from './ReportActionItemMessageEdit'; import ReportActionItemSingle from './ReportActionItemSingle'; import ReportActionItemThread from './ReportActionItemThread'; -import reportActionPropTypes from './reportActionPropTypes'; import ReportAttachmentsContext from './ReportAttachmentsContext'; -const propTypes = { - ...windowDimensionsPropTypes, +type ReportActionItemOnyxProps = { + /** Stores user's preferred skin tone */ + preferredSkinTone: OnyxEntry; + + /** IOU report for this action, if any */ + iouReport: OnyxEntry; + + emojiReactions: OnyxEntry; + /** The user's wallet account */ + userWallet: OnyxEntry; + + /** All the report actions belonging to the report's parent */ + parentReportActions: OnyxEntry; +}; + +type ReportActionItemProps = { /** Report for this action */ - report: reportPropTypes.isRequired, + report: OnyxTypes.Report; /** All the data of the action item */ - action: PropTypes.shape(reportActionPropTypes).isRequired, + action: OnyxTypes.ReportAction; /** Should the comment have the appearance of being grouped with the previous comment? */ - displayAsGroup: PropTypes.bool.isRequired, + displayAsGroup: boolean; /** Is this the most recent IOU Action? */ - isMostRecentIOUReportAction: PropTypes.bool.isRequired, + isMostRecentIOUReportAction: boolean; /** Should we display the new marker on top of the comment? */ - shouldDisplayNewMarker: PropTypes.bool.isRequired, + shouldDisplayNewMarker: boolean; /** Determines if the avatar is displayed as a subscript (positioned lower than normal) */ - shouldShowSubscriptAvatar: PropTypes.bool, + shouldShowSubscriptAvatar?: boolean; /** Position index of the report action in the overall report FlatList view */ - index: PropTypes.number.isRequired, + index: number; /** Draft message - if this is set the comment is in 'edit' mode */ - draftMessage: PropTypes.string, - - /** Stores user's preferred skin tone */ - preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - - ...windowDimensionsPropTypes, - emojiReactions: EmojiReactionsPropTypes, - - /** IOU report for this action, if any */ - iouReport: reportPropTypes, + draftMessage?: string; /** Flag to show, hide the thread divider line */ - shouldHideThreadDividerLine: PropTypes.bool, - - /** The user's wallet account */ - userWallet: userWalletPropTypes, - - /** All the report actions belonging to the report's parent */ - parentReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), -}; - -const defaultProps = { - draftMessage: undefined, - preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, - emojiReactions: {}, - shouldShowSubscriptAvatar: false, - iouReport: undefined, - shouldHideThreadDividerLine: false, - userWallet: {}, - parentReportActions: {}, -}; - -function ReportActionItem(props) { + shouldHideThreadDividerLine?: boolean; + + linkedReportActionID?: string; +} & ReportActionItemOnyxProps; + +function ReportActionItem({ + action, + report, + draftMessage = undefined, + linkedReportActionID, + displayAsGroup, + emojiReactions, + index, + iouReport = undefined, + isMostRecentIOUReportAction, + parentReportActions, + preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, + shouldDisplayNewMarker, + userWallet, + shouldHideThreadDividerLine = false, + shouldShowSubscriptAvatar = false, +}: ReportActionItemProps) { + const {translate} = useLocalize(); + const {} = useWindowDimensions(); const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; - const [isContextMenuActive, setIsContextMenuActive] = useState(() => ReportActionContextMenu.isActiveReportAction(props.action.reportActionID)); + const [isContextMenuActive, setIsContextMenuActive] = useState(() => ReportActionContextMenu.isActiveReportAction(action.reportActionID)); const [isHidden, setIsHidden] = useState(false); - const [moderationDecision, setModerationDecision] = useState(CONST.MODERATION.MODERATOR_DECISION_APPROVED); + const [moderationDecision, setModerationDecision] = useState(CONST.MODERATION.MODERATOR_DECISION_APPROVED); const reactionListRef = useContext(ReactionListContext); const {updateHiddenAttachments} = useContext(ReportAttachmentsContext); const textInputRef = useRef(); - const popoverAnchorRef = useRef(); - const downloadedPreviews = useRef([]); - const prevDraftMessage = usePrevious(props.draftMessage); - const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action); - const originalReport = props.report.reportID === originalReportID ? props.report : ReportUtils.getReport(originalReportID); - const isReportActionLinked = props.linkedReportActionID === props.action.reportActionID; + const popoverAnchorRef = useRef(null); + const downloadedPreviews = useRef([]); + const prevDraftMessage = usePrevious(draftMessage); + const originalReportID = ReportUtils.getOriginalReportID(report.reportID, action); + const originalReport = report.reportID === originalReportID ? report : ReportUtils.getReport(originalReportID); + const isReportActionLinked = linkedReportActionID === action.reportActionID; const highlightedBackgroundColorIfNeeded = useMemo( () => (isReportActionLinked ? StyleUtils.getBackgroundColorStyle(theme.hoverComponentBG) : {}), [StyleUtils, isReportActionLinked, theme.hoverComponentBG], ); - const originalMessage = lodashGet(props.action, 'originalMessage', {}); - const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(props.action); + const originalMessage = action.originalMessage ?? {}; + const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(action); // IOUDetails only exists when we are sending money - const isSendingMoney = originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && _.has(originalMessage, 'IOUDetails'); + const isSendingMoney = action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && action.originalMessage.IOUDetails; const updateHiddenState = useCallback( - (isHiddenValue) => { + (isHiddenValue: boolean) => { setIsHidden(isHiddenValue); - const isAttachment = ReportUtils.isReportMessageAttachment(_.last(props.action.message)); + const isAttachment = ReportUtils.isReportMessageAttachment(action.message?.[action.message?.length - 1]); if (!isAttachment) { return; } - updateHiddenAttachments(props.action.reportActionID, isHiddenValue); + updateHiddenAttachments(action.reportActionID, isHiddenValue); }, - [props.action.reportActionID, props.action.message, updateHiddenAttachments], + [action.reportActionID, action.message, updateHiddenAttachments], ); useEffect( () => () => { // ReportActionContextMenu, EmojiPicker and PopoverReactionList are global components, // we should also hide them when the current component is destroyed - if (ReportActionContextMenu.isActiveReportAction(props.action.reportActionID)) { + if (ReportActionContextMenu.isActiveReportAction(action.reportActionID)) { ReportActionContextMenu.hideContextMenu(); ReportActionContextMenu.hideDeleteModal(); } - if (EmojiPickerAction.isActive(props.action.reportActionID)) { + if (EmojiPickerAction.isActive(action.reportActionID)) { EmojiPickerAction.hideEmojiPicker(true); } - if (reactionListRef.current && reactionListRef.current.isActiveReportAction(props.action.reportActionID)) { - reactionListRef.current.hideReactionList(); + if (reactionListRef?.current?.isActiveReportAction(action.reportActionID)) { + reactionListRef?.current?.hideReactionList(); } }, - [props.action.reportActionID, reactionListRef], + [action.reportActionID, reactionListRef], ); useEffect(() => { // We need to hide EmojiPicker when this is a deleted parent action - if (!isDeletedParentAction || !EmojiPickerAction.isActive(props.action.reportActionID)) { + if (!isDeletedParentAction || !EmojiPickerAction.isActive(action.reportActionID)) { return; } EmojiPickerAction.hideEmojiPicker(true); - }, [isDeletedParentAction, props.action.reportActionID]); + }, [isDeletedParentAction, action.reportActionID]); useEffect(() => { - if (!_.isUndefined(prevDraftMessage) || _.isUndefined(props.draftMessage)) { + if (!!prevDraftMessage || !draftMessage) { return; } focusTextInputAfterAnimation(textInputRef.current, 100); - }, [prevDraftMessage, props.draftMessage]); + }, [prevDraftMessage, draftMessage]); useEffect(() => { if (!Permissions.canUseLinkPreviews()) { return; } - const urls = ReportActionsUtils.extractLinksFromMessageHtml(props.action); - if (_.isEqual(downloadedPreviews.current, urls) || props.action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + const urls = ReportActionsUtils.extractLinksFromMessageHtml(action); + if (_.isEqual(downloadedPreviews.current, urls) || action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { return; } downloadedPreviews.current = urls; - Report.expandURLPreview(props.report.reportID, props.action.reportActionID); - }, [props.action, props.report.reportID]); + Report.expandURLPreview(report.reportID, action.reportActionID); + }, [action, report.reportID]); useEffect(() => { - if (_.isUndefined(props.draftMessage) || !ReportActionsUtils.isDeletedAction(props.action)) { + if (!draftMessage || !ReportActionsUtils.isDeletedAction(action)) { return; } - Report.deleteReportActionDraft(props.report.reportID, props.action); - }, [props.draftMessage, props.action, props.report.reportID]); + Report.deleteReportActionDraft(report.reportID, action); + }, [draftMessage, action, report.reportID]); // Hide the message if it is being moderated for a higher offense, or is hidden by a moderator // Removed messages should not be shown anyway and should not need this flow - const latestDecision = lodashGet(props, ['action', 'message', 0, 'moderationDecision', 'decision'], ''); + const latestDecision = action.message?.[0].moderationDecision?.decision ?? ''; useEffect(() => { - if (props.action.actionName !== CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT) { + if (action.actionName !== CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT) { return; } // Hide reveal message button and show the message if latestDecision is changed to empty - if (_.isEmpty(latestDecision)) { + if (!latestDecision) { setModerationDecision(CONST.MODERATION.MODERATOR_DECISION_APPROVED); setIsHidden(false); return; } setModerationDecision(latestDecision); - if (!_.contains([CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING], latestDecision)) { + if (![CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING].some((item) => item === latestDecision)) { setIsHidden(true); return; } setIsHidden(false); - }, [latestDecision, props.action.actionName]); + }, [latestDecision, action.actionName]); const toggleContextMenuFromActiveReportAction = useCallback(() => { - setIsContextMenuActive(ReportActionContextMenu.isActiveReportAction(props.action.reportActionID)); - }, [props.action.reportActionID]); + setIsContextMenuActive(ReportActionContextMenu.isActiveReportAction(action.reportActionID)); + }, [action.reportActionID]); /** * Show the ReportActionContextMenu modal popover. @@ -264,7 +269,7 @@ function ReportActionItem(props) { const showPopover = useCallback( (event) => { // Block menu on the message being Edited or if the report action item has errors - if (!_.isUndefined(props.draftMessage) || !_.isEmpty(props.action.errors)) { + if (!!draftMessage || !isEmptyObject(action.errors)) { return; } @@ -274,77 +279,77 @@ function ReportActionItem(props) { CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, event, selection, - popoverAnchorRef, - props.report.reportID, - props.action.reportActionID, + popoverAnchorRef.current, + report.reportID, + action.reportActionID, originalReportID, - props.draftMessage, + draftMessage ?? '', () => setIsContextMenuActive(true), toggleContextMenuFromActiveReportAction, ReportUtils.isArchivedRoom(originalReport), ReportUtils.chatIncludesChronos(originalReport), ); }, - [props.draftMessage, props.action, props.report.reportID, toggleContextMenuFromActiveReportAction, originalReport, originalReportID], + [draftMessage, action, report.reportID, toggleContextMenuFromActiveReportAction, originalReport, originalReportID], ); const toggleReaction = useCallback( (emoji) => { - Report.toggleEmojiReaction(props.report.reportID, props.action, emoji, props.emojiReactions); + Report.toggleEmojiReaction(report.reportID, action, emoji, emojiReactions ?? undefined); }, - [props.report, props.action, props.emojiReactions], + [report, action, emojiReactions], ); const contextValue = useMemo( () => ({ anchor: popoverAnchorRef, - report: props.report, - action: props.action, + report, + action, checkIfContextMenuActive: toggleContextMenuFromActiveReportAction, }), - [props.report, props.action, toggleContextMenuFromActiveReportAction], + [report, action, toggleContextMenuFromActiveReportAction], ); /** * Get the content of ReportActionItem - * @param {Boolean} hovered whether the ReportActionItem is hovered - * @param {Boolean} isWhisper whether the report action is a whisper - * @param {Boolean} hasErrors whether the report action has any errors - * @returns {Object} child component(s) + * @param hovered whether the ReportActionItem is hovered + * @param isWhisper whether the report action is a whisper + * @param hasErrors whether the report action has any errors + * @returns child component(s) */ const renderItemContent = (hovered = false, isWhisper = false, hasErrors = false) => { let children; // Show the MoneyRequestPreview for when request was created, bill was split or money was sent if ( - props.action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && - originalMessage && + action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && + action.originalMessage && // For the pay flow, we only want to show MoneyRequestAction when sending money. When paying, we display a regular system message - (originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT || isSendingMoney) + (action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT || isSendingMoney) ) { // There is no single iouReport for bill splits, so only 1:1 requests require an iouReportID - const iouReportID = originalMessage.IOUReportID ? originalMessage.IOUReportID.toString() : '0'; + const iouReportID = action.originalMessage.IOUReportID ? action.originalMessage.IOUReportID.toString() : '0'; children = ( ); - } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW) { + } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW) { children = ( ); } else if ( - props.action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED || - props.action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED || - props.action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED + action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED || + action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED || + action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED ) { - children = ; - } else if (ReportActionsUtils.isCreatedTaskReportAction(props.action)) { + children = ; + } else if (ReportActionsUtils.isCreatedTaskReportAction(action)) { children = ( ); - } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED) { - const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(lodashGet(personalDetails, props.report.ownerAccountID)); - const paymentType = lodashGet(props.action, 'originalMessage.paymentType', ''); + } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED) { + const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails[report.ownerAccountID ?? -1]); + const paymentType = action.originalMessage.paymentType ?? ''; - const isSubmitterOfUnsettledReport = ReportUtils.isCurrentUserSubmitter(props.report.reportID) && !ReportUtils.isSettled(props.report.reportID); + const isSubmitterOfUnsettledReport = ReportUtils.isCurrentUserSubmitter(report.reportID) && !ReportUtils.isSettled(report.reportID); const shouldShowAddCreditBankAccountButton = isSubmitterOfUnsettledReport && !store.hasCreditBankAccount() && paymentType !== CONST.IOU.PAYMENT_TYPE.EXPENSIFY; const shouldShowEnableWalletButton = - isSubmitterOfUnsettledReport && - (_.isEmpty(props.userWallet) || props.userWallet.tierName === CONST.WALLET.TIER_NAME.SILVER) && - paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY; + isSubmitterOfUnsettledReport && (isEmptyObject(userWallet) || userWallet?.tierName === CONST.WALLET.TIER_NAME.SILVER) && paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY; children = ( <> {shouldShowAddCreditBankAccountButton && ( )} ) : ( )} ); } - const numberOfThreadReplies = _.get(props, ['action', 'childVisibleActionCount'], 0); + const numberOfThreadReplies = action.childVisibleActionCount ?? 0; - const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(props.action, props.report.reportID); - const oldestFourAccountIDs = _.map(lodashGet(props.action, 'childOldestFourAccountIDs', '').split(','), (accountID) => Number(accountID)); - const draftMessageRightAlign = !_.isUndefined(props.draftMessage) ? styles.chatItemReactionsDraftRight : {}; + const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(action, report.reportID); + const oldestFourAccountIDs = action.childOldestFourAccountIDs?.split(',').map((accountID) => Number(accountID)); + const draftMessageRightAlign = draftMessage ? styles.chatItemReactionsDraftRight : {}; return ( <> {children} - {Permissions.canUseLinkPreviews() && !isHidden && !_.isEmpty(props.action.linkMetadata) && ( - - !_.isEmpty(item))} /> + {Permissions.canUseLinkPreviews() && !isHidden && action.linkMetadata.lenght > 0 && ( + + !_.isEmpty(item))} /> )} - {!ReportActionsUtils.isMessageDeleted(props.action) && ( + {!ReportActionsUtils.isMessageDeleted(action) && ( { if (Session.isAnonymousUser()) { @@ -524,9 +522,9 @@ function ReportActionItem(props) { {shouldDisplayThreadReplies && ( { const content = renderItemContent(hovered || isContextMenuActive, isWhisper, hasErrors); - if (!_.isUndefined(props.draftMessage)) { + if (!_.isUndefined(draftMessage)) { return {content}; } - if (!props.displayAsGroup) { + if (!displayAsGroup) { return ( @@ -571,30 +569,30 @@ function ReportActionItem(props) { return {content}; }; - if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { - const parentReportAction = props.parentReportActions[props.report.parentReportActionID]; + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { + const parentReportAction = parentReportActions[report.parentReportActionID]; if (ReportActionsUtils.isTransactionThread(parentReportAction)) { return ( ); } - if (ReportUtils.isTaskReport(props.report)) { - if (ReportUtils.isCanceledTaskReport(props.report, parentReportAction)) { + if (ReportUtils.isTaskReport(report)) { + if (ReportUtils.isCanceledTaskReport(report, parentReportAction)) { return ( <> - + - ${props.translate('parentReportAction.deletedTask')}`} /> + ${translate('parentReportAction.deletedTask')}`} /> @@ -604,21 +602,21 @@ function ReportActionItem(props) { return ( <> - + ); } - if (ReportUtils.isExpenseReport(props.report) || ReportUtils.isIOUReport(props.report)) { + if (ReportUtils.isExpenseReport(report) || ReportUtils.isIOUReport(report)) { return ( - + ); @@ -626,19 +624,19 @@ function ReportActionItem(props) { return ( ); } - if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) { - return ; + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) { + return ; } - if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.CHRONOSOOOLIST) { + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.CHRONOSOOOLIST) { return ( ); } @@ -646,8 +644,8 @@ function ReportActionItem(props) { // For the `pay` IOU action on non-send money flow, we don't want to render anything if `isWaitingOnBankAccount` is true // Otherwise, we will see two system messages informing the payee needs to add a bank account or wallet if ( - props.action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && - lodashGet(props.report, 'isWaitingOnBankAccount', false) && + action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && + lodashGet(report, 'isWaitingOnBankAccount', false) && originalMessage && originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && !isSendingMoney @@ -655,8 +653,8 @@ function ReportActionItem(props) { return null; } - const hasErrors = !_.isEmpty(props.action.errors); - const whisperedToAccountIDs = props.action.whisperedToAccountIDs || []; + const hasErrors = !_.isEmpty(action.errors); + const whisperedToAccountIDs = action.whisperedToAccountIDs || []; const isWhisper = whisperedToAccountIDs.length > 0; const isMultipleParticipant = whisperedToAccountIDs.length > 1; const isWhisperOnlyVisibleByUser = isWhisper && ReportUtils.isCurrentUserTheOnlyParticipant(whisperedToAccountIDs); @@ -665,41 +663,39 @@ function ReportActionItem(props) { return ( props.isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + style={[action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ? styles.pointerEventsNone : styles.pointerEventsAuto]} + onPressIn={() => isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} onSecondaryInteraction={showPopover} - preventDefaultContextMenu={_.isUndefined(props.draftMessage) && !hasErrors} + preventDefaultContextMenu={_.isUndefined(draftMessage) && !hasErrors} withoutFocusOnSecondaryInteraction - accessibilityLabel={props.translate('accessibilityHints.chatMessage')} + accessibilityLabel={translate('accessibilityHints.chatMessage')} > {(hovered) => ( - {props.shouldDisplayNewMarker && } + {shouldDisplayNewMarker && } - + ReportActions.clearReportActionErrors(props.report.reportID, props.action)} - pendingAction={ - !_.isUndefined(props.draftMessage) ? null : props.action.pendingAction || (props.action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : '') - } - shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(props.action, props.report.reportID)} - errors={props.action.errors} + onClose={() => ReportActions.clearReportActionErrors(report.reportID, action)} + pendingAction={!_.isUndefined(draftMessage) ? null : action.pendingAction || (action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : '')} + shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(action, report.reportID)} + errors={action.errors} errorRowStyles={[styles.ml10, styles.mr2]} - needsOffscreenAlphaCompositing={ReportActionsUtils.isMoneyRequestAction(props.action)} + needsOffscreenAlphaCompositing={ReportActionsUtils.isMoneyRequestAction(action)} shouldDisableStrikeThrough > {isWhisper && ( @@ -712,7 +708,7 @@ function ReportActionItem(props) { /> - {props.translate('reportActionContextMenu.onlyVisible')} + {translate('reportActionContextMenu.onlyVisible')}   - + ); } -ReportActionItem.propTypes = propTypes; -ReportActionItem.defaultProps = defaultProps; - export default compose( - withWindowDimensions, - withLocalize, withNetwork(), withBlockedFromConcierge({propName: 'blockedFromConcierge'}), withReportActionsDrafts({ propName: 'draftMessage', transformValue: (drafts, props) => { - const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action); + const originalReportID = ReportUtils.getOriginalReportID(report.reportID, action); const draftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`; - return lodashGet(drafts, [draftKey, props.action.reportActionID, 'message']); + return lodashGet(drafts, [draftKey, action.reportActionID, 'message']); }, }), withOnyx({ diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index c4e30157bf6f..c8ed2eb8d53f 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -235,6 +235,11 @@ type OriginalMessageMoved = { }; }; +type OriginalMessageMarkedReimbursement = { + actionName: typeof CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSEMENT; + originalMessage: unknown; +}; + type OriginalMessage = | OriginalMessageApproved | OriginalMessageIOU @@ -251,7 +256,8 @@ type OriginalMessage = | OriginalMessageModifiedExpense | OriginalMessageReimbursementQueued | OriginalMessageReimbursementDequeued - | OriginalMessageMoved; + | OriginalMessageMoved + | OriginalMessageMarkedReimbursement; export default OriginalMessage; export type { @@ -266,4 +272,5 @@ export type { OriginalMessageIOU, OriginalMessageCreated, OriginalMessageAddComment, + DecisionName, }; From 457ad4c9938f64f7f05f7c6d5e0e079974e5cdbc Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 12 Jan 2024 09:02:24 +0100 Subject: [PATCH 022/583] Revert "ref: wip" This reverts commit 1c13e82db29b037d16d7a8a0b8db8df158182495. --- src/libs/ReportUtils.ts | 2 +- .../focusTextInputAfterAnimation/index.ts | 2 +- .../focusTextInputAfterAnimation/types.ts | 2 +- src/libs/isReportMessageAttachment.ts | 10 +- ...portActionItem.tsx => ReportActionItem.js} | 487 +++++++++--------- src/types/onyx/OriginalMessage.ts | 9 +- 6 files changed, 257 insertions(+), 255 deletions(-) rename src/pages/home/report/{ReportActionItem.tsx => ReportActionItem.js} (62%) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 18a70b9aae48..1010f8bd82e0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3559,7 +3559,7 @@ function getAllPolicyReports(policyID: string): Array> { /** * Returns true if Chronos is one of the chat participants (1:1) */ -function chatIncludesChronos(report: OnyxEntry | EmptyObject): boolean { +function chatIncludesChronos(report: OnyxEntry): boolean { return Boolean(report?.participantAccountIDs?.includes(CONST.ACCOUNT_ID.CHRONOS)); } diff --git a/src/libs/focusTextInputAfterAnimation/index.ts b/src/libs/focusTextInputAfterAnimation/index.ts index 66d0c35c1a63..3f7c6555b5ce 100644 --- a/src/libs/focusTextInputAfterAnimation/index.ts +++ b/src/libs/focusTextInputAfterAnimation/index.ts @@ -4,7 +4,7 @@ import type FocusTextInputAfterAnimation from './types'; * This library is a no-op for all platforms except for Android and iOS and will immediately focus the given input without any delays. */ const focusTextInputAfterAnimation: FocusTextInputAfterAnimation = (inputRef) => { - inputRef?.focus(); + inputRef.focus(); }; export default focusTextInputAfterAnimation; diff --git a/src/libs/focusTextInputAfterAnimation/types.ts b/src/libs/focusTextInputAfterAnimation/types.ts index bfe29317c1ef..a6a14165598b 100644 --- a/src/libs/focusTextInputAfterAnimation/types.ts +++ b/src/libs/focusTextInputAfterAnimation/types.ts @@ -1,5 +1,5 @@ import type {TextInput} from 'react-native'; -type FocusTextInputAfterAnimation = (inputRef: TextInput | HTMLInputElement | undefined, animationLength: number) => void; +type FocusTextInputAfterAnimation = (inputRef: TextInput | HTMLInputElement, animationLength: number) => void; export default FocusTextInputAfterAnimation; diff --git a/src/libs/isReportMessageAttachment.ts b/src/libs/isReportMessageAttachment.ts index 5ff4bfbef093..df8a589f7bdc 100644 --- a/src/libs/isReportMessageAttachment.ts +++ b/src/libs/isReportMessageAttachment.ts @@ -7,15 +7,15 @@ import type {Message} from '@src/types/onyx/ReportAction'; * * @param reportActionMessage report action's message as text, html and translationKey */ -export default function isReportMessageAttachment(message: Message | undefined): boolean { - if (!message?.text || !message?.html) { +export default function isReportMessageAttachment({text, html, translationKey}: Message): boolean { + if (!text || !html) { return false; } - if (message?.translationKey && message?.text === CONST.ATTACHMENT_MESSAGE_TEXT) { - return message?.translationKey === CONST.TRANSLATION_KEYS.ATTACHMENT; + if (translationKey && text === CONST.ATTACHMENT_MESSAGE_TEXT) { + return translationKey === CONST.TRANSLATION_KEYS.ATTACHMENT; } const regex = new RegExp(` ${CONST.ATTACHMENT_SOURCE_ATTRIBUTE}="(.*)"`, 'i'); - return message?.text === CONST.ATTACHMENT_MESSAGE_TEXT && (!!message?.html.match(regex) || message?.html === CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML); + return text === CONST.ATTACHMENT_MESSAGE_TEXT && (!!html.match(regex) || html === CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML); } diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.js similarity index 62% rename from src/pages/home/report/ReportActionItem.tsx rename to src/pages/home/report/ReportActionItem.js index 8e42612c260e..b1130af5d2ff 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.js @@ -1,7 +1,9 @@ +import lodashGet from 'lodash/get'; +import PropTypes from 'prop-types'; import React, {memo, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager, 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 DisplayNames from '@components/DisplayNames'; import Hoverable from '@components/Hoverable'; @@ -12,6 +14,7 @@ import KYCWall from '@components/KYCWall'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {usePersonalDetails, withBlockedFromConcierge, withNetwork, withReportActionsDrafts} from '@components/OnyxProvider'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; +import EmojiReactionsPropTypes from '@components/Reactions/EmojiReactionsPropTypes'; import ReportActionItemEmojiReactions from '@components/Reactions/ReportActionItemEmojiReactions'; import RenderHTML from '@components/RenderHTML'; import ChronosOOOListActions from '@components/ReportActionItem/ChronosOOOListActions'; @@ -26,12 +29,12 @@ import TaskView from '@components/ReportActionItem/TaskView'; import {ShowContextMenuContext} from '@components/ShowContextMenuContext'; import Text from '@components/Text'; import UnreadActionIndicator from '@components/UnreadActionIndicator'; -import useLocalize from '@hooks/useLocalize'; +import withLocalize from '@components/withLocalize'; +import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import usePrevious from '@hooks/usePrevious'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import ControlSelection from '@libs/ControlSelection'; import * as CurrencyUtils from '@libs/CurrencyUtils'; @@ -44,7 +47,9 @@ import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import SelectionScraper from '@libs/SelectionScraper'; +import userWalletPropTypes from '@pages/EnablePayments/userWalletPropTypes'; import {ReactionListContext} from '@pages/home/ReportScreenContext'; +import reportPropTypes from '@pages/reportPropTypes'; import * as BankAccounts from '@userActions/BankAccounts'; import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; import * as store from '@userActions/ReimbursementAccount/store'; @@ -55,9 +60,6 @@ import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type * as OnyxTypes from '@src/types/onyx'; -import type {DecisionName} from '@src/types/onyx/OriginalMessage'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; import MiniReportActionContextMenu from './ContextMenu/MiniReportActionContextMenu'; import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu'; @@ -71,195 +73,188 @@ import ReportActionItemMessage from './ReportActionItemMessage'; import ReportActionItemMessageEdit from './ReportActionItemMessageEdit'; import ReportActionItemSingle from './ReportActionItemSingle'; import ReportActionItemThread from './ReportActionItemThread'; +import reportActionPropTypes from './reportActionPropTypes'; import ReportAttachmentsContext from './ReportAttachmentsContext'; -type ReportActionItemOnyxProps = { - /** Stores user's preferred skin tone */ - preferredSkinTone: OnyxEntry; - - /** IOU report for this action, if any */ - iouReport: OnyxEntry; - - emojiReactions: OnyxEntry; +const propTypes = { + ...windowDimensionsPropTypes, - /** The user's wallet account */ - userWallet: OnyxEntry; - - /** All the report actions belonging to the report's parent */ - parentReportActions: OnyxEntry; -}; - -type ReportActionItemProps = { /** Report for this action */ - report: OnyxTypes.Report; + report: reportPropTypes.isRequired, /** All the data of the action item */ - action: OnyxTypes.ReportAction; + action: PropTypes.shape(reportActionPropTypes).isRequired, /** Should the comment have the appearance of being grouped with the previous comment? */ - displayAsGroup: boolean; + displayAsGroup: PropTypes.bool.isRequired, /** Is this the most recent IOU Action? */ - isMostRecentIOUReportAction: boolean; + isMostRecentIOUReportAction: PropTypes.bool.isRequired, /** Should we display the new marker on top of the comment? */ - shouldDisplayNewMarker: boolean; + shouldDisplayNewMarker: PropTypes.bool.isRequired, /** Determines if the avatar is displayed as a subscript (positioned lower than normal) */ - shouldShowSubscriptAvatar?: boolean; + shouldShowSubscriptAvatar: PropTypes.bool, /** Position index of the report action in the overall report FlatList view */ - index: number; + index: PropTypes.number.isRequired, /** Draft message - if this is set the comment is in 'edit' mode */ - draftMessage?: string; + draftMessage: PropTypes.string, + + /** Stores user's preferred skin tone */ + preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + + ...windowDimensionsPropTypes, + emojiReactions: EmojiReactionsPropTypes, + + /** IOU report for this action, if any */ + iouReport: reportPropTypes, /** Flag to show, hide the thread divider line */ - shouldHideThreadDividerLine?: boolean; - - linkedReportActionID?: string; -} & ReportActionItemOnyxProps; - -function ReportActionItem({ - action, - report, - draftMessage = undefined, - linkedReportActionID, - displayAsGroup, - emojiReactions, - index, - iouReport = undefined, - isMostRecentIOUReportAction, - parentReportActions, - preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, - shouldDisplayNewMarker, - userWallet, - shouldHideThreadDividerLine = false, - shouldShowSubscriptAvatar = false, -}: ReportActionItemProps) { - const {translate} = useLocalize(); - const {} = useWindowDimensions(); + shouldHideThreadDividerLine: PropTypes.bool, + + /** The user's wallet account */ + userWallet: userWalletPropTypes, + + /** All the report actions belonging to the report's parent */ + parentReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), +}; + +const defaultProps = { + draftMessage: undefined, + preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, + emojiReactions: {}, + shouldShowSubscriptAvatar: false, + iouReport: undefined, + shouldHideThreadDividerLine: false, + userWallet: {}, + parentReportActions: {}, +}; + +function ReportActionItem(props) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; - const [isContextMenuActive, setIsContextMenuActive] = useState(() => ReportActionContextMenu.isActiveReportAction(action.reportActionID)); + const [isContextMenuActive, setIsContextMenuActive] = useState(() => ReportActionContextMenu.isActiveReportAction(props.action.reportActionID)); const [isHidden, setIsHidden] = useState(false); - const [moderationDecision, setModerationDecision] = useState(CONST.MODERATION.MODERATOR_DECISION_APPROVED); + const [moderationDecision, setModerationDecision] = useState(CONST.MODERATION.MODERATOR_DECISION_APPROVED); const reactionListRef = useContext(ReactionListContext); const {updateHiddenAttachments} = useContext(ReportAttachmentsContext); const textInputRef = useRef(); - const popoverAnchorRef = useRef(null); - const downloadedPreviews = useRef([]); - const prevDraftMessage = usePrevious(draftMessage); - const originalReportID = ReportUtils.getOriginalReportID(report.reportID, action); - const originalReport = report.reportID === originalReportID ? report : ReportUtils.getReport(originalReportID); - const isReportActionLinked = linkedReportActionID === action.reportActionID; + const popoverAnchorRef = useRef(); + const downloadedPreviews = useRef([]); + const prevDraftMessage = usePrevious(props.draftMessage); + const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action); + const originalReport = props.report.reportID === originalReportID ? props.report : ReportUtils.getReport(originalReportID); + const isReportActionLinked = props.linkedReportActionID === props.action.reportActionID; const highlightedBackgroundColorIfNeeded = useMemo( () => (isReportActionLinked ? StyleUtils.getBackgroundColorStyle(theme.hoverComponentBG) : {}), [StyleUtils, isReportActionLinked, theme.hoverComponentBG], ); - const originalMessage = action.originalMessage ?? {}; - const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(action); + const originalMessage = lodashGet(props.action, 'originalMessage', {}); + const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(props.action); // IOUDetails only exists when we are sending money - const isSendingMoney = action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && action.originalMessage.IOUDetails; + const isSendingMoney = originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && _.has(originalMessage, 'IOUDetails'); const updateHiddenState = useCallback( - (isHiddenValue: boolean) => { + (isHiddenValue) => { setIsHidden(isHiddenValue); - const isAttachment = ReportUtils.isReportMessageAttachment(action.message?.[action.message?.length - 1]); + const isAttachment = ReportUtils.isReportMessageAttachment(_.last(props.action.message)); if (!isAttachment) { return; } - updateHiddenAttachments(action.reportActionID, isHiddenValue); + updateHiddenAttachments(props.action.reportActionID, isHiddenValue); }, - [action.reportActionID, action.message, updateHiddenAttachments], + [props.action.reportActionID, props.action.message, updateHiddenAttachments], ); useEffect( () => () => { // ReportActionContextMenu, EmojiPicker and PopoverReactionList are global components, // we should also hide them when the current component is destroyed - if (ReportActionContextMenu.isActiveReportAction(action.reportActionID)) { + if (ReportActionContextMenu.isActiveReportAction(props.action.reportActionID)) { ReportActionContextMenu.hideContextMenu(); ReportActionContextMenu.hideDeleteModal(); } - if (EmojiPickerAction.isActive(action.reportActionID)) { + if (EmojiPickerAction.isActive(props.action.reportActionID)) { EmojiPickerAction.hideEmojiPicker(true); } - if (reactionListRef?.current?.isActiveReportAction(action.reportActionID)) { - reactionListRef?.current?.hideReactionList(); + if (reactionListRef.current && reactionListRef.current.isActiveReportAction(props.action.reportActionID)) { + reactionListRef.current.hideReactionList(); } }, - [action.reportActionID, reactionListRef], + [props.action.reportActionID, reactionListRef], ); useEffect(() => { // We need to hide EmojiPicker when this is a deleted parent action - if (!isDeletedParentAction || !EmojiPickerAction.isActive(action.reportActionID)) { + if (!isDeletedParentAction || !EmojiPickerAction.isActive(props.action.reportActionID)) { return; } EmojiPickerAction.hideEmojiPicker(true); - }, [isDeletedParentAction, action.reportActionID]); + }, [isDeletedParentAction, props.action.reportActionID]); useEffect(() => { - if (!!prevDraftMessage || !draftMessage) { + if (!_.isUndefined(prevDraftMessage) || _.isUndefined(props.draftMessage)) { return; } focusTextInputAfterAnimation(textInputRef.current, 100); - }, [prevDraftMessage, draftMessage]); + }, [prevDraftMessage, props.draftMessage]); useEffect(() => { if (!Permissions.canUseLinkPreviews()) { return; } - const urls = ReportActionsUtils.extractLinksFromMessageHtml(action); - if (_.isEqual(downloadedPreviews.current, urls) || action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + const urls = ReportActionsUtils.extractLinksFromMessageHtml(props.action); + if (_.isEqual(downloadedPreviews.current, urls) || props.action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { return; } downloadedPreviews.current = urls; - Report.expandURLPreview(report.reportID, action.reportActionID); - }, [action, report.reportID]); + Report.expandURLPreview(props.report.reportID, props.action.reportActionID); + }, [props.action, props.report.reportID]); useEffect(() => { - if (!draftMessage || !ReportActionsUtils.isDeletedAction(action)) { + if (_.isUndefined(props.draftMessage) || !ReportActionsUtils.isDeletedAction(props.action)) { return; } - Report.deleteReportActionDraft(report.reportID, action); - }, [draftMessage, action, report.reportID]); + Report.deleteReportActionDraft(props.report.reportID, props.action); + }, [props.draftMessage, props.action, props.report.reportID]); // Hide the message if it is being moderated for a higher offense, or is hidden by a moderator // Removed messages should not be shown anyway and should not need this flow - const latestDecision = action.message?.[0].moderationDecision?.decision ?? ''; + const latestDecision = lodashGet(props, ['action', 'message', 0, 'moderationDecision', 'decision'], ''); useEffect(() => { - if (action.actionName !== CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT) { + if (props.action.actionName !== CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT) { return; } // Hide reveal message button and show the message if latestDecision is changed to empty - if (!latestDecision) { + if (_.isEmpty(latestDecision)) { setModerationDecision(CONST.MODERATION.MODERATOR_DECISION_APPROVED); setIsHidden(false); return; } setModerationDecision(latestDecision); - if (![CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING].some((item) => item === latestDecision)) { + if (!_.contains([CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING], latestDecision)) { setIsHidden(true); return; } setIsHidden(false); - }, [latestDecision, action.actionName]); + }, [latestDecision, props.action.actionName]); const toggleContextMenuFromActiveReportAction = useCallback(() => { - setIsContextMenuActive(ReportActionContextMenu.isActiveReportAction(action.reportActionID)); - }, [action.reportActionID]); + setIsContextMenuActive(ReportActionContextMenu.isActiveReportAction(props.action.reportActionID)); + }, [props.action.reportActionID]); /** * Show the ReportActionContextMenu modal popover. @@ -269,7 +264,7 @@ function ReportActionItem({ const showPopover = useCallback( (event) => { // Block menu on the message being Edited or if the report action item has errors - if (!!draftMessage || !isEmptyObject(action.errors)) { + if (!_.isUndefined(props.draftMessage) || !_.isEmpty(props.action.errors)) { return; } @@ -279,77 +274,77 @@ function ReportActionItem({ CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, event, selection, - popoverAnchorRef.current, - report.reportID, - action.reportActionID, + popoverAnchorRef, + props.report.reportID, + props.action.reportActionID, originalReportID, - draftMessage ?? '', + props.draftMessage, () => setIsContextMenuActive(true), toggleContextMenuFromActiveReportAction, ReportUtils.isArchivedRoom(originalReport), ReportUtils.chatIncludesChronos(originalReport), ); }, - [draftMessage, action, report.reportID, toggleContextMenuFromActiveReportAction, originalReport, originalReportID], + [props.draftMessage, props.action, props.report.reportID, toggleContextMenuFromActiveReportAction, originalReport, originalReportID], ); const toggleReaction = useCallback( (emoji) => { - Report.toggleEmojiReaction(report.reportID, action, emoji, emojiReactions ?? undefined); + Report.toggleEmojiReaction(props.report.reportID, props.action, emoji, props.emojiReactions); }, - [report, action, emojiReactions], + [props.report, props.action, props.emojiReactions], ); const contextValue = useMemo( () => ({ anchor: popoverAnchorRef, - report, - action, + report: props.report, + action: props.action, checkIfContextMenuActive: toggleContextMenuFromActiveReportAction, }), - [report, action, toggleContextMenuFromActiveReportAction], + [props.report, props.action, toggleContextMenuFromActiveReportAction], ); /** * Get the content of ReportActionItem - * @param hovered whether the ReportActionItem is hovered - * @param isWhisper whether the report action is a whisper - * @param hasErrors whether the report action has any errors - * @returns child component(s) + * @param {Boolean} hovered whether the ReportActionItem is hovered + * @param {Boolean} isWhisper whether the report action is a whisper + * @param {Boolean} hasErrors whether the report action has any errors + * @returns {Object} child component(s) */ const renderItemContent = (hovered = false, isWhisper = false, hasErrors = false) => { let children; // Show the MoneyRequestPreview for when request was created, bill was split or money was sent if ( - action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && - action.originalMessage && + props.action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && + originalMessage && // For the pay flow, we only want to show MoneyRequestAction when sending money. When paying, we display a regular system message - (action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT || isSendingMoney) + (originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT || isSendingMoney) ) { // There is no single iouReport for bill splits, so only 1:1 requests require an iouReportID - const iouReportID = action.originalMessage.IOUReportID ? action.originalMessage.IOUReportID.toString() : '0'; + const iouReportID = originalMessage.IOUReportID ? originalMessage.IOUReportID.toString() : '0'; children = ( ); - } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW) { + } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW) { children = ( ); } else if ( - action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED || - action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED || - action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED + props.action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED || + props.action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED || + props.action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED ) { - children = ; - } else if (ReportActionsUtils.isCreatedTaskReportAction(action)) { + children = ; + } else if (ReportActionsUtils.isCreatedTaskReportAction(props.action)) { children = ( ); - } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED) { - const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails[report.ownerAccountID ?? -1]); - const paymentType = action.originalMessage.paymentType ?? ''; + } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED) { + const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(lodashGet(personalDetails, props.report.ownerAccountID)); + const paymentType = lodashGet(props.action, 'originalMessage.paymentType', ''); - const isSubmitterOfUnsettledReport = ReportUtils.isCurrentUserSubmitter(report.reportID) && !ReportUtils.isSettled(report.reportID); + const isSubmitterOfUnsettledReport = ReportUtils.isCurrentUserSubmitter(props.report.reportID) && !ReportUtils.isSettled(props.report.reportID); const shouldShowAddCreditBankAccountButton = isSubmitterOfUnsettledReport && !store.hasCreditBankAccount() && paymentType !== CONST.IOU.PAYMENT_TYPE.EXPENSIFY; const shouldShowEnableWalletButton = - isSubmitterOfUnsettledReport && (isEmptyObject(userWallet) || userWallet?.tierName === CONST.WALLET.TIER_NAME.SILVER) && paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY; + isSubmitterOfUnsettledReport && + (_.isEmpty(props.userWallet) || props.userWallet.tierName === CONST.WALLET.TIER_NAME.SILVER) && + paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY; children = ( <> {shouldShowAddCreditBankAccountButton && ( )} ) : ( )} ); } - const numberOfThreadReplies = action.childVisibleActionCount ?? 0; + const numberOfThreadReplies = _.get(props, ['action', 'childVisibleActionCount'], 0); - const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(action, report.reportID); - const oldestFourAccountIDs = action.childOldestFourAccountIDs?.split(',').map((accountID) => Number(accountID)); - const draftMessageRightAlign = draftMessage ? styles.chatItemReactionsDraftRight : {}; + const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(props.action, props.report.reportID); + const oldestFourAccountIDs = _.map(lodashGet(props.action, 'childOldestFourAccountIDs', '').split(','), (accountID) => Number(accountID)); + const draftMessageRightAlign = !_.isUndefined(props.draftMessage) ? styles.chatItemReactionsDraftRight : {}; return ( <> {children} - {Permissions.canUseLinkPreviews() && !isHidden && action.linkMetadata.lenght > 0 && ( - - !_.isEmpty(item))} /> + {Permissions.canUseLinkPreviews() && !isHidden && !_.isEmpty(props.action.linkMetadata) && ( + + !_.isEmpty(item))} /> )} - {!ReportActionsUtils.isMessageDeleted(action) && ( + {!ReportActionsUtils.isMessageDeleted(props.action) && ( { if (Session.isAnonymousUser()) { @@ -522,9 +524,9 @@ function ReportActionItem({ {shouldDisplayThreadReplies && ( { const content = renderItemContent(hovered || isContextMenuActive, isWhisper, hasErrors); - if (!_.isUndefined(draftMessage)) { + if (!_.isUndefined(props.draftMessage)) { return {content}; } - if (!displayAsGroup) { + if (!props.displayAsGroup) { return ( @@ -569,30 +571,30 @@ function ReportActionItem({ return {content}; }; - if (action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { - const parentReportAction = parentReportActions[report.parentReportActionID]; + if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { + const parentReportAction = props.parentReportActions[props.report.parentReportActionID]; if (ReportActionsUtils.isTransactionThread(parentReportAction)) { return ( ); } - if (ReportUtils.isTaskReport(report)) { - if (ReportUtils.isCanceledTaskReport(report, parentReportAction)) { + if (ReportUtils.isTaskReport(props.report)) { + if (ReportUtils.isCanceledTaskReport(props.report, parentReportAction)) { return ( <> - + - ${translate('parentReportAction.deletedTask')}`} /> + ${props.translate('parentReportAction.deletedTask')}`} /> @@ -602,21 +604,21 @@ function ReportActionItem({ return ( <> - + ); } - if (ReportUtils.isExpenseReport(report) || ReportUtils.isIOUReport(report)) { + if (ReportUtils.isExpenseReport(props.report) || ReportUtils.isIOUReport(props.report)) { return ( - + ); @@ -624,19 +626,19 @@ function ReportActionItem({ return ( ); } - if (action.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) { - return ; + if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) { + return ; } - if (action.actionName === CONST.REPORT.ACTIONS.TYPE.CHRONOSOOOLIST) { + if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.CHRONOSOOOLIST) { return ( ); } @@ -644,8 +646,8 @@ function ReportActionItem({ // For the `pay` IOU action on non-send money flow, we don't want to render anything if `isWaitingOnBankAccount` is true // Otherwise, we will see two system messages informing the payee needs to add a bank account or wallet if ( - action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && - lodashGet(report, 'isWaitingOnBankAccount', false) && + props.action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && + lodashGet(props.report, 'isWaitingOnBankAccount', false) && originalMessage && originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && !isSendingMoney @@ -653,8 +655,8 @@ function ReportActionItem({ return null; } - const hasErrors = !_.isEmpty(action.errors); - const whisperedToAccountIDs = action.whisperedToAccountIDs || []; + const hasErrors = !_.isEmpty(props.action.errors); + const whisperedToAccountIDs = props.action.whisperedToAccountIDs || []; const isWhisper = whisperedToAccountIDs.length > 0; const isMultipleParticipant = whisperedToAccountIDs.length > 1; const isWhisperOnlyVisibleByUser = isWhisper && ReportUtils.isCurrentUserTheOnlyParticipant(whisperedToAccountIDs); @@ -663,39 +665,41 @@ function ReportActionItem({ return ( isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + style={[props.action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ? styles.pointerEventsNone : styles.pointerEventsAuto]} + onPressIn={() => props.isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} onSecondaryInteraction={showPopover} - preventDefaultContextMenu={_.isUndefined(draftMessage) && !hasErrors} + preventDefaultContextMenu={_.isUndefined(props.draftMessage) && !hasErrors} withoutFocusOnSecondaryInteraction - accessibilityLabel={translate('accessibilityHints.chatMessage')} + accessibilityLabel={props.translate('accessibilityHints.chatMessage')} > {(hovered) => ( - {shouldDisplayNewMarker && } + {props.shouldDisplayNewMarker && } - + ReportActions.clearReportActionErrors(report.reportID, action)} - pendingAction={!_.isUndefined(draftMessage) ? null : action.pendingAction || (action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : '')} - shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(action, report.reportID)} - errors={action.errors} + onClose={() => ReportActions.clearReportActionErrors(props.report.reportID, props.action)} + pendingAction={ + !_.isUndefined(props.draftMessage) ? null : props.action.pendingAction || (props.action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : '') + } + shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(props.action, props.report.reportID)} + errors={props.action.errors} errorRowStyles={[styles.ml10, styles.mr2]} - needsOffscreenAlphaCompositing={ReportActionsUtils.isMoneyRequestAction(action)} + needsOffscreenAlphaCompositing={ReportActionsUtils.isMoneyRequestAction(props.action)} shouldDisableStrikeThrough > {isWhisper && ( @@ -708,7 +712,7 @@ function ReportActionItem({ /> - {translate('reportActionContextMenu.onlyVisible')} + {props.translate('reportActionContextMenu.onlyVisible')}   - + ); } +ReportActionItem.propTypes = propTypes; +ReportActionItem.defaultProps = defaultProps; + export default compose( + withWindowDimensions, + withLocalize, withNetwork(), withBlockedFromConcierge({propName: 'blockedFromConcierge'}), withReportActionsDrafts({ propName: 'draftMessage', transformValue: (drafts, props) => { - const originalReportID = ReportUtils.getOriginalReportID(report.reportID, action); + const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action); const draftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`; - return lodashGet(drafts, [draftKey, action.reportActionID, 'message']); + return lodashGet(drafts, [draftKey, props.action.reportActionID, 'message']); }, }), withOnyx({ diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index c8ed2eb8d53f..c4e30157bf6f 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -235,11 +235,6 @@ type OriginalMessageMoved = { }; }; -type OriginalMessageMarkedReimbursement = { - actionName: typeof CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSEMENT; - originalMessage: unknown; -}; - type OriginalMessage = | OriginalMessageApproved | OriginalMessageIOU @@ -256,8 +251,7 @@ type OriginalMessage = | OriginalMessageModifiedExpense | OriginalMessageReimbursementQueued | OriginalMessageReimbursementDequeued - | OriginalMessageMoved - | OriginalMessageMarkedReimbursement; + | OriginalMessageMoved; export default OriginalMessage; export type { @@ -272,5 +266,4 @@ export type { OriginalMessageIOU, OriginalMessageCreated, OriginalMessageAddComment, - DecisionName, }; From 0882361e5b901b328a99b061a35a6068504abf25 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 12 Jan 2024 14:50:16 +0100 Subject: [PATCH 023/583] ref: wip --- .../ReportActionItemEmojiReactions.tsx | 2 +- .../ContextMenu/ReportActionContextMenu.ts | 2 +- src/pages/home/report/ReportActionItem.tsx | 90 ++++++++++--------- .../report/ReportActionItemMessageEdit.tsx | 2 +- .../home/report/ReportActionItemSingle.tsx | 27 +++--- 5 files changed, 67 insertions(+), 56 deletions(-) diff --git a/src/components/Reactions/ReportActionItemEmojiReactions.tsx b/src/components/Reactions/ReportActionItemEmojiReactions.tsx index 69779dc316e1..5f08430b67ce 100644 --- a/src/components/Reactions/ReportActionItemEmojiReactions.tsx +++ b/src/components/Reactions/ReportActionItemEmojiReactions.tsx @@ -23,7 +23,7 @@ type ReportActionItemEmojiReactionsProps = WithCurrentUserPersonalDetailsProps & emojiReactions: OnyxEntry; /** The user's preferred locale. */ - preferredLocale: OnyxEntry; + preferredLocale?: OnyxEntry; /** The report action that these reactions are for */ reportAction: ReportAction; diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts index 5b64d90da5da..de1ec23a6720 100644 --- a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts +++ b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts @@ -100,7 +100,7 @@ function showContextMenu( reportID = '0', reportActionID = '0', originalReportID = '0', - draftMessage = undefined, + draftMessage: string | undefined = undefined, onShow = () => {}, onHide = () => {}, isArchivedRoom = false, diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index 8e42612c260e..ecb5dc2f3c2a 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -1,7 +1,8 @@ import React, {memo, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; -import {InteractionManager, View} from 'react-native'; +import {GestureResponderEvent, InteractionManager, TextInput, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import {Emoji} from '@assets/emojis/types'; import Button from '@components/Button'; import DisplayNames from '@components/DisplayNames'; import Hoverable from '@components/Hoverable'; @@ -56,7 +57,8 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; -import type {DecisionName} from '@src/types/onyx/OriginalMessage'; +import type {DecisionName, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; +import {ReportActionBase} from '@src/types/onyx/ReportAction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; import MiniReportActionContextMenu from './ContextMenu/MiniReportActionContextMenu'; @@ -120,6 +122,8 @@ type ReportActionItemProps = { linkedReportActionID?: string; } & ReportActionItemOnyxProps; +const isIOUReport = (actionObj: OnyxEntry): actionObj is ReportActionBase & OriginalMessageIOU => actionObj?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU; + function ReportActionItem({ action, report, @@ -128,7 +132,7 @@ function ReportActionItem({ displayAsGroup, emojiReactions, index, - iouReport = undefined, + iouReport, isMostRecentIOUReportAction, parentReportActions, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, @@ -138,7 +142,7 @@ function ReportActionItem({ shouldShowSubscriptAvatar = false, }: ReportActionItemProps) { const {translate} = useLocalize(); - const {} = useWindowDimensions(); + const {isSmallScreenWidth} = useWindowDimensions(); const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -148,23 +152,22 @@ function ReportActionItem({ const [moderationDecision, setModerationDecision] = useState(CONST.MODERATION.MODERATOR_DECISION_APPROVED); const reactionListRef = useContext(ReactionListContext); const {updateHiddenAttachments} = useContext(ReportAttachmentsContext); - const textInputRef = useRef(); + const textInputRef = useRef(); const popoverAnchorRef = useRef(null); const downloadedPreviews = useRef([]); const prevDraftMessage = usePrevious(draftMessage); const originalReportID = ReportUtils.getOriginalReportID(report.reportID, action); const originalReport = report.reportID === originalReportID ? report : ReportUtils.getReport(originalReportID); const isReportActionLinked = linkedReportActionID === action.reportActionID; - + console.log('hello', action.actionName, action.originalMessage); const highlightedBackgroundColorIfNeeded = useMemo( () => (isReportActionLinked ? StyleUtils.getBackgroundColorStyle(theme.hoverComponentBG) : {}), [StyleUtils, isReportActionLinked, theme.hoverComponentBG], ); - const originalMessage = action.originalMessage ?? {}; const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(action); // IOUDetails only exists when we are sending money - const isSendingMoney = action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && action.originalMessage.IOUDetails; + const isSendingMoney = isIOUReport(action) && action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && action.originalMessage.IOUDetails; const updateHiddenState = useCallback( (isHiddenValue: boolean) => { @@ -219,7 +222,10 @@ function ReportActionItem({ } const urls = ReportActionsUtils.extractLinksFromMessageHtml(action); - if (_.isEqual(downloadedPreviews.current, urls) || action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + if ( + (downloadedPreviews.current.length === urls.length && downloadedPreviews.current.every((value, arrIndex) => value === urls[arrIndex])) || + action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE + ) { return; } @@ -267,7 +273,7 @@ function ReportActionItem({ * @param {Object} [event] - A press event. */ const showPopover = useCallback( - (event) => { + (event: GestureResponderEvent | MouseEvent) => { // Block menu on the message being Edited or if the report action item has errors if (!!draftMessage || !isEmptyObject(action.errors)) { return; @@ -294,7 +300,7 @@ function ReportActionItem({ ); const toggleReaction = useCallback( - (emoji) => { + (emoji: Emoji) => { Report.toggleEmojiReaction(report.reportID, action, emoji, emojiReactions ?? undefined); }, [report, action, emojiReactions], @@ -487,15 +493,19 @@ function ReportActionItem({ const numberOfThreadReplies = action.childVisibleActionCount ?? 0; const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(action, report.reportID); - const oldestFourAccountIDs = action.childOldestFourAccountIDs?.split(',').map((accountID) => Number(accountID)); + const oldestFourAccountIDs = action.childOldestFourAccountIDs + ?.split(',') + .map((accountID) => Number(accountID)) + // eslint-disable-next-line no-restricted-globals + .filter((accountID): accountID is number => !isNaN(accountID)); const draftMessageRightAlign = draftMessage ? styles.chatItemReactionsDraftRight : {}; return ( <> {children} - {Permissions.canUseLinkPreviews() && !isHidden && action.linkMetadata.lenght > 0 && ( + {Permissions.canUseLinkPreviews() && !isHidden && (action.linkMetadata?.length ?? 0) > 0 && ( - !_.isEmpty(item))} /> + !isEmptyObject(item))} /> )} {!ReportActionsUtils.isMessageDeleted(action) && ( @@ -537,15 +547,15 @@ function ReportActionItem({ /** * Get ReportActionItem with a proper wrapper - * @param {Boolean} hovered whether the ReportActionItem is hovered - * @param {Boolean} isWhisper whether the ReportActionItem is a whisper - * @param {Boolean} hasErrors whether the report action has any errors - * @returns {Object} report action item + * @param hovered whether the ReportActionItem is hovered + * @param isWhisper whether the ReportActionItem is a whisper + * @param hasErrors whether the report action has any errors + * @returns report action item */ - const renderReportActionItem = (hovered, isWhisper, hasErrors) => { + const renderReportActionItem = (hovered: boolean, isWhisper: boolean, hasErrors: boolean) => { const content = renderItemContent(hovered || isContextMenuActive, isWhisper, hasErrors); - if (!_.isUndefined(draftMessage)) { + if (draftMessage) { return {content}; } @@ -553,13 +563,13 @@ function ReportActionItem({ return ( item === moderationDecision)} > {content} @@ -570,7 +580,7 @@ function ReportActionItem({ }; if (action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { - const parentReportAction = parentReportActions[report.parentReportActionID]; + const parentReportAction = parentReportActions?.[report.parentReportActionID ?? ''] ?? null; if (ReportActionsUtils.isTransactionThread(parentReportAction)) { return ( @@ -589,7 +599,7 @@ function ReportActionItem({ ${translate('parentReportAction.deletedTask')}`} /> @@ -645,7 +655,7 @@ function ReportActionItem({ // Otherwise, we will see two system messages informing the payee needs to add a bank account or wallet if ( action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && - lodashGet(report, 'isWaitingOnBankAccount', false) && + !!report?.isWaitingOnBankAccount && originalMessage && originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && !isSendingMoney @@ -658,7 +668,7 @@ function ReportActionItem({ const isWhisper = whisperedToAccountIDs.length > 0; const isMultipleParticipant = whisperedToAccountIDs.length > 1; const isWhisperOnlyVisibleByUser = isWhisper && ReportUtils.isCurrentUserTheOnlyParticipant(whisperedToAccountIDs); - const whisperedToPersonalDetails = isWhisper ? _.filter(personalDetails, (details) => _.includes(whisperedToAccountIDs, details.accountID)) : []; + const whisperedToPersonalDetails = isWhisper ? Object.values(personalDetails ?? {}).filter((details) => whisperedToAccountIDs.includes(details?.accountID ?? -1)) : []; const displayNamesWithTooltips = isWhisper ? ReportUtils.getDisplayNamesWithTooltips(whisperedToPersonalDetails, isMultipleParticipant) : []; return ( isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} onSecondaryInteraction={showPopover} - preventDefaultContextMenu={_.isUndefined(draftMessage) && !hasErrors} + preventDefaultContextMenu={!draftMessage && !hasErrors} withoutFocusOnSecondaryInteraction accessibilityLabel={translate('accessibilityHints.chatMessage')} > {(hovered) => ( @@ -681,17 +691,18 @@ function ReportActionItem({ - + ReportActions.clearReportActionErrors(report.reportID, action)} - pendingAction={!_.isUndefined(draftMessage) ? null : action.pendingAction || (action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : '')} + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + pendingAction={draftMessage ? null : action.pendingAction || (action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : '')} shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(action, report.reportID)} errors={action.errors} errorRowStyles={[styles.ml10, styles.mr2]} @@ -712,7 +723,7 @@ function ReportActionItem({   { - const originalReportID = ReportUtils.getOriginalReportID(report.reportID, action); + const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action); const draftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`; - return lodashGet(drafts, [draftKey, action.reportActionID, 'message']); + console.log('hej', drafts?.[draftKey][props.action.reportActionID].message); + return drafts?.[draftKey][props.action.reportActionID].message; }, }), - withOnyx({ + withOnyx({ preferredSkinTone: { key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, initialValue: CONST.EMOJI_DEFAULT_SKIN_TONE, @@ -753,19 +765,17 @@ export default compose( iouReport: { key: ({action}) => { const iouReportID = ReportActionsUtils.getIOUReportIDFromReportActionPreview(action); - return iouReportID ? `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}` : undefined; + return `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`; }, - initialValue: {}, }, emojiReactions: { key: ({action}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${action.reportActionID}`, - initialValue: {}, }, userWallet: { key: ONYXKEYS.USER_WALLET, }, parentReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID || 0}`, + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID ?? ''}`, canEvict: false, }, }), diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 5934c4c333cb..96e3e20e5f03 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -71,7 +71,7 @@ const isMobileSafari = Browser.isMobileSafari(); function ReportActionItemMessageEdit( {action, draftMessage, reportID, index, shouldDisableEmojiPicker = false, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE}: ReportActionItemMessageEditProps, - forwardedRef: ForwardedRef, + forwardedRef: ForwardedRef<(TextInput & HTMLTextAreaElement) | undefined>, ) { const theme = useTheme(); const styles = useThemeStyles(); diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 6c4e2b39a329..78afe2116503 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -1,6 +1,7 @@ import React, {useCallback, useMemo} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import Avatar from '@components/Avatar'; import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -29,7 +30,7 @@ import ReportActionItemFragment from './ReportActionItemFragment'; type ReportActionItemSingleProps = ChildrenProps & { /** All the data of the action */ - action: ReportAction; + action: OnyxEntry; /** Styles for the outermost View */ wrapperStyle?: StyleProp; @@ -38,7 +39,7 @@ type ReportActionItemSingleProps = ChildrenProps & { report: Report; /** IOU Report for this action, if any */ - iouReport?: Report; + iouReport?: OnyxEntry; /** Show header for action */ showHeader?: boolean; @@ -77,12 +78,12 @@ function ReportActionItemSingle({ const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; - const actorAccountID = action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport ? iouReport.managerID : action.actorAccountID; + const actorAccountID = action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport ? iouReport.managerID : action?.actorAccountID; let displayName = ReportUtils.getDisplayNameForParticipant(actorAccountID); const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails[actorAccountID ?? -1] ?? {}; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); - const displayAllActors = useMemo(() => action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport, [action.actionName, iouReport]); + const displayAllActors = useMemo(() => action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport, [action?.actionName, iouReport]); const isWorkspaceActor = ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors); let avatarSource = UserUtils.getAvatar(avatar ?? '', actorAccountID); @@ -90,7 +91,7 @@ function ReportActionItemSingle({ displayName = ReportUtils.getPolicyName(report); actorHint = displayName; avatarSource = ReportUtils.getWorkspaceAvatar(report); - } else if (action.delegateAccountID && personalDetails[action.delegateAccountID]) { + } else if (action?.delegateAccountID && personalDetails[action?.delegateAccountID]) { // We replace the actor's email, name, and avatar with the Copilot manually for now. And only if we have their // details. This will be improved upon when the Copilot feature is implemented. const delegateDetails = personalDetails[action.delegateAccountID]; @@ -141,7 +142,7 @@ function ReportActionItemSingle({ text: displayName, }, ] - : action.person; + : action?.person; const reportID = report?.reportID; const iouReportID = iouReport?.reportID; @@ -155,14 +156,14 @@ function ReportActionItemSingle({ Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(iouReportID)); return; } - showUserDetails(action.delegateAccountID ? action.delegateAccountID : String(actorAccountID)); + showUserDetails(action?.delegateAccountID ? action?.delegateAccountID : String(actorAccountID)); } - }, [isWorkspaceActor, reportID, actorAccountID, action.delegateAccountID, iouReportID, displayAllActors]); + }, [isWorkspaceActor, reportID, actorAccountID, action?.delegateAccountID, iouReportID, displayAllActors]); const shouldDisableDetailPage = useMemo( () => actorAccountID === CONST.ACCOUNT_ID.NOTIFICATIONS || - (!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(action.delegateAccountID ? Number(action.delegateAccountID) : actorAccountID ?? -1)), + (!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(action?.delegateAccountID ? Number(action?.delegateAccountID) : actorAccountID ?? -1)), [action, isWorkspaceActor, actorAccountID], ); @@ -189,7 +190,7 @@ function ReportActionItemSingle({ return ( @@ -237,10 +238,10 @@ function ReportActionItemSingle({ {personArray?.map((fragment, index) => ( @@ -254,7 +255,7 @@ function ReportActionItemSingle({ >{`${status?.emojiCode}`} )} - + ) : null} {children} From 36f3081340b519f7c88cc21a271f227614b20b9a Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 12 Jan 2024 19:52:31 +0100 Subject: [PATCH 024/583] ref: wip --- .../types.ts | 2 +- .../ReportActionItem/TaskPreview.tsx | 3 +- src/libs/ReportUtils.ts | 2 +- src/libs/actions/User.ts | 6 +- .../focusTextInputAfterAnimation/index.ts | 2 +- .../focusTextInputAfterAnimation/types.ts | 2 +- src/libs/isReportMessageAttachment.ts | 10 +- ...portActionItem.js => ReportActionItem.tsx} | 598 +++++++++--------- .../report/ReportActionItemBasicMessage.tsx | 2 +- .../report/ReportActionItemMessageEdit.tsx | 3 +- .../home/report/ReportActionItemThread.tsx | 3 +- src/types/onyx/OriginalMessage.ts | 9 +- 12 files changed, 332 insertions(+), 310 deletions(-) rename src/pages/home/report/{ReportActionItem.js => ReportActionItem.tsx} (56%) diff --git a/src/components/PressableWithSecondaryInteraction/types.ts b/src/components/PressableWithSecondaryInteraction/types.ts index aa67d45d66fb..b07c867daeb3 100644 --- a/src/components/PressableWithSecondaryInteraction/types.ts +++ b/src/components/PressableWithSecondaryInteraction/types.ts @@ -4,7 +4,7 @@ import type {ParsableStyle} from '@styles/utils/types'; type PressableWithSecondaryInteractionProps = PressableWithFeedbackProps & { /** The function that should be called when this pressable is pressed */ - onPress: (event?: GestureResponderEvent) => void; + onPress?: (event?: GestureResponderEvent) => void; /** The function that should be called when this pressable is pressedIn */ onPressIn?: (event?: GestureResponderEvent) => void; diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index fbc58a381318..a2a4897d19dd 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -63,7 +63,7 @@ type TaskPreviewProps = WithCurrentUserPersonalDetailsProps & chatReportID: string; /** Popover context menu anchor, used for showing context menu */ - contextMenuAnchor: Element; + contextMenuAnchor: View | null; /** Callback for updating context menu active state, used for showing context menu */ checkIfContextMenuActive: () => void; @@ -111,6 +111,7 @@ function TaskPreview({ onPress={() => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(taskReportID))} onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} + // @ts-expect-error TODO: Remove this once ShowContextMenuContext (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. onLongPress={(event) => showContextMenuForReport(event, contextMenuAnchor, chatReportID, action ?? {}, checkIfContextMenuActive)} style={[styles.flexRow, styles.justifyContentBetween]} role={CONST.ROLE.BUTTON} diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 181ce5461dd7..b01c703732dc 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3559,7 +3559,7 @@ function getAllPolicyReports(policyID: string): Array> { /** * Returns true if Chronos is one of the chat participants (1:1) */ -function chatIncludesChronos(report: OnyxEntry): boolean { +function chatIncludesChronos(report: OnyxEntry | EmptyObject): boolean { return Boolean(report?.participantAccountIDs?.includes(CONST.ACCOUNT_ID.CHRONOS)); } diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 8e3bd5f2c017..e0f3003ed9e8 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -13,7 +13,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {FrequentlyUsedEmoji} from '@src/types/onyx'; +import type {BlockedFromConcierge, FrequentlyUsedEmoji} from '@src/types/onyx'; import type Login from '@src/types/onyx/Login'; import type {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer'; import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails'; @@ -27,8 +27,6 @@ import * as PersonalDetails from './PersonalDetails'; import * as Report from './Report'; import * as Session from './Session'; -type BlockedFromConciergeNVP = {expiresAt: number}; - let currentUserAccountID = -1; let currentEmail = ''; Onyx.connect({ @@ -445,7 +443,7 @@ function validateSecondaryLogin(contactMethod: string, validateCode: string) { * and if so whether the expiresAt date of a user's ban is before right now * */ -function isBlockedFromConcierge(blockedFromConciergeNVP: OnyxEntry): boolean { +function isBlockedFromConcierge(blockedFromConciergeNVP: OnyxEntry): boolean { if (isEmptyObject(blockedFromConciergeNVP)) { return false; } diff --git a/src/libs/focusTextInputAfterAnimation/index.ts b/src/libs/focusTextInputAfterAnimation/index.ts index 3f7c6555b5ce..66d0c35c1a63 100644 --- a/src/libs/focusTextInputAfterAnimation/index.ts +++ b/src/libs/focusTextInputAfterAnimation/index.ts @@ -4,7 +4,7 @@ import type FocusTextInputAfterAnimation from './types'; * This library is a no-op for all platforms except for Android and iOS and will immediately focus the given input without any delays. */ const focusTextInputAfterAnimation: FocusTextInputAfterAnimation = (inputRef) => { - inputRef.focus(); + inputRef?.focus(); }; export default focusTextInputAfterAnimation; diff --git a/src/libs/focusTextInputAfterAnimation/types.ts b/src/libs/focusTextInputAfterAnimation/types.ts index a6a14165598b..bfe29317c1ef 100644 --- a/src/libs/focusTextInputAfterAnimation/types.ts +++ b/src/libs/focusTextInputAfterAnimation/types.ts @@ -1,5 +1,5 @@ import type {TextInput} from 'react-native'; -type FocusTextInputAfterAnimation = (inputRef: TextInput | HTMLInputElement, animationLength: number) => void; +type FocusTextInputAfterAnimation = (inputRef: TextInput | HTMLInputElement | undefined, animationLength: number) => void; export default FocusTextInputAfterAnimation; diff --git a/src/libs/isReportMessageAttachment.ts b/src/libs/isReportMessageAttachment.ts index df8a589f7bdc..5ff4bfbef093 100644 --- a/src/libs/isReportMessageAttachment.ts +++ b/src/libs/isReportMessageAttachment.ts @@ -7,15 +7,15 @@ import type {Message} from '@src/types/onyx/ReportAction'; * * @param reportActionMessage report action's message as text, html and translationKey */ -export default function isReportMessageAttachment({text, html, translationKey}: Message): boolean { - if (!text || !html) { +export default function isReportMessageAttachment(message: Message | undefined): boolean { + if (!message?.text || !message?.html) { return false; } - if (translationKey && text === CONST.ATTACHMENT_MESSAGE_TEXT) { - return translationKey === CONST.TRANSLATION_KEYS.ATTACHMENT; + if (message?.translationKey && message?.text === CONST.ATTACHMENT_MESSAGE_TEXT) { + return message?.translationKey === CONST.TRANSLATION_KEYS.ATTACHMENT; } const regex = new RegExp(` ${CONST.ATTACHMENT_SOURCE_ATTRIBUTE}="(.*)"`, 'i'); - return text === CONST.ATTACHMENT_MESSAGE_TEXT && (!!html.match(regex) || html === CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML); + return message?.text === CONST.ATTACHMENT_MESSAGE_TEXT && (!!message?.html.match(regex) || message?.html === CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML); } diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.tsx similarity index 56% rename from src/pages/home/report/ReportActionItem.js rename to src/pages/home/report/ReportActionItem.tsx index 2ece2e0eb7ce..350d72942be7 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.tsx @@ -1,9 +1,10 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import lodashIsEqual from 'lodash/isEqual'; import React, {memo, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; +import type {GestureResponderEvent, TextInput} from 'react-native'; import {InteractionManager, View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {Emoji} from '@assets/emojis/types'; import Button from '@components/Button'; import DisplayNames from '@components/DisplayNames'; import Hoverable from '@components/Hoverable'; @@ -14,7 +15,6 @@ import KYCWall from '@components/KYCWall'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {usePersonalDetails, withBlockedFromConcierge, withNetwork, withReportActionsDrafts} from '@components/OnyxProvider'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; -import EmojiReactionsPropTypes from '@components/Reactions/EmojiReactionsPropTypes'; import ReportActionItemEmojiReactions from '@components/Reactions/ReportActionItemEmojiReactions'; import RenderHTML from '@components/RenderHTML'; import ChronosOOOListActions from '@components/ReportActionItem/ChronosOOOListActions'; @@ -29,12 +29,12 @@ import TaskView from '@components/ReportActionItem/TaskView'; import {ShowContextMenuContext} from '@components/ShowContextMenuContext'; import Text from '@components/Text'; import UnreadActionIndicator from '@components/UnreadActionIndicator'; -import withLocalize from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; +import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import ControlSelection from '@libs/ControlSelection'; import * as CurrencyUtils from '@libs/CurrencyUtils'; @@ -47,9 +47,7 @@ import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import SelectionScraper from '@libs/SelectionScraper'; -import userWalletPropTypes from '@pages/EnablePayments/userWalletPropTypes'; import {ReactionListContext} from '@pages/home/ReportScreenContext'; -import reportPropTypes from '@pages/reportPropTypes'; import * as BankAccounts from '@userActions/BankAccounts'; import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; import * as store from '@userActions/ReimbursementAccount/store'; @@ -60,6 +58,10 @@ import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {DecisionName, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; +import type {ReportActionBase} from '@src/types/onyx/ReportAction'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; import MiniReportActionContextMenu from './ContextMenu/MiniReportActionContextMenu'; import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu'; @@ -73,188 +75,202 @@ import ReportActionItemMessage from './ReportActionItemMessage'; import ReportActionItemMessageEdit from './ReportActionItemMessageEdit'; import ReportActionItemSingle from './ReportActionItemSingle'; import ReportActionItemThread from './ReportActionItemThread'; -import reportActionPropTypes from './reportActionPropTypes'; import ReportAttachmentsContext from './ReportAttachmentsContext'; -const propTypes = { - ...windowDimensionsPropTypes, +type ReportActionItemOnyxProps = { + /** Stores user's preferred skin tone */ + preferredSkinTone: OnyxEntry; + + /** IOU report for this action, if any */ + iouReport: OnyxEntry; + + emojiReactions: OnyxEntry; + + /** The user's wallet account */ + userWallet: OnyxEntry; + /** All the report actions belonging to the report's parent */ + parentReportActions: OnyxEntry; +}; + +type ReportActionItemProps = { /** Report for this action */ - report: reportPropTypes.isRequired, + report: OnyxTypes.Report; /** All the data of the action item */ - action: PropTypes.shape(reportActionPropTypes).isRequired, + action: OnyxTypes.ReportAction; /** Should the comment have the appearance of being grouped with the previous comment? */ - displayAsGroup: PropTypes.bool.isRequired, + displayAsGroup: boolean; /** Is this the most recent IOU Action? */ - isMostRecentIOUReportAction: PropTypes.bool.isRequired, + isMostRecentIOUReportAction: boolean; /** Should we display the new marker on top of the comment? */ - shouldDisplayNewMarker: PropTypes.bool.isRequired, + shouldDisplayNewMarker: boolean; /** Determines if the avatar is displayed as a subscript (positioned lower than normal) */ - shouldShowSubscriptAvatar: PropTypes.bool, + shouldShowSubscriptAvatar?: boolean; /** Position index of the report action in the overall report FlatList view */ - index: PropTypes.number.isRequired, + index: number; /** Draft message - if this is set the comment is in 'edit' mode */ - draftMessage: PropTypes.string, - - /** Stores user's preferred skin tone */ - preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - - ...windowDimensionsPropTypes, - emojiReactions: EmojiReactionsPropTypes, - - /** IOU report for this action, if any */ - iouReport: reportPropTypes, + draftMessage?: string; /** Flag to show, hide the thread divider line */ - shouldHideThreadDividerLine: PropTypes.bool, - - /** The user's wallet account */ - userWallet: userWalletPropTypes, - - /** All the report actions belonging to the report's parent */ - parentReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), -}; - -const defaultProps = { - draftMessage: undefined, - preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, - emojiReactions: {}, - shouldShowSubscriptAvatar: false, - iouReport: undefined, - shouldHideThreadDividerLine: false, - userWallet: {}, - parentReportActions: {}, -}; - -function ReportActionItem(props) { + shouldHideThreadDividerLine?: boolean; + + linkedReportActionID?: string; + + blockedFromConcierge: OnyxTypes.BlockedFromConcierge; +} & ReportActionItemOnyxProps; + +const isIOUReport = (actionObj: OnyxEntry): actionObj is ReportActionBase & OriginalMessageIOU => actionObj?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU; + +function ReportActionItem({ + action, + report, + draftMessage = undefined, + linkedReportActionID, + displayAsGroup, + emojiReactions, + index, + iouReport, + isMostRecentIOUReportAction, + parentReportActions, + preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, + shouldDisplayNewMarker, + userWallet, + shouldHideThreadDividerLine = false, + shouldShowSubscriptAvatar = false, + blockedFromConcierge, +}: ReportActionItemProps) { + const {translate} = useLocalize(); + const {isSmallScreenWidth} = useWindowDimensions(); const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; - const [isContextMenuActive, setIsContextMenuActive] = useState(() => ReportActionContextMenu.isActiveReportAction(props.action.reportActionID)); + const [isContextMenuActive, setIsContextMenuActive] = useState(() => ReportActionContextMenu.isActiveReportAction(action.reportActionID)); const [isHidden, setIsHidden] = useState(false); - const [moderationDecision, setModerationDecision] = useState(CONST.MODERATION.MODERATOR_DECISION_APPROVED); + const [moderationDecision, setModerationDecision] = useState(CONST.MODERATION.MODERATOR_DECISION_APPROVED); const reactionListRef = useContext(ReactionListContext); const {updateHiddenAttachments} = useContext(ReportAttachmentsContext); - const textInputRef = useRef(); - const popoverAnchorRef = useRef(); - const downloadedPreviews = useRef([]); - const prevDraftMessage = usePrevious(props.draftMessage); - const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action); - const originalReport = props.report.reportID === originalReportID ? props.report : ReportUtils.getReport(originalReportID); - const isReportActionLinked = props.linkedReportActionID === props.action.reportActionID; - + const textInputRef = useRef(); + const popoverAnchorRef = useRef(null); + const downloadedPreviews = useRef([]); + const prevDraftMessage = usePrevious(draftMessage); + const originalReportID = ReportUtils.getOriginalReportID(report.reportID, action); + const originalReport = report.reportID === originalReportID ? report : ReportUtils.getReport(originalReportID); + const isReportActionLinked = linkedReportActionID === action.reportActionID; + console.log('hello', action.actionName, action.originalMessage); const highlightedBackgroundColorIfNeeded = useMemo( () => (isReportActionLinked ? StyleUtils.getBackgroundColorStyle(theme.hoverComponentBG) : {}), [StyleUtils, isReportActionLinked, theme.hoverComponentBG], ); - const originalMessage = lodashGet(props.action, 'originalMessage', {}); - const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(props.action); + const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(action); // IOUDetails only exists when we are sending money - const isSendingMoney = originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && _.has(originalMessage, 'IOUDetails'); + const isSendingMoney = isIOUReport(action) && action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && action.originalMessage.IOUDetails; const updateHiddenState = useCallback( - (isHiddenValue) => { + (isHiddenValue: boolean) => { setIsHidden(isHiddenValue); - const isAttachment = ReportUtils.isReportMessageAttachment(_.last(props.action.message)); + const isAttachment = ReportUtils.isReportMessageAttachment(action.message?.[action.message?.length - 1]); if (!isAttachment) { return; } - updateHiddenAttachments(props.action.reportActionID, isHiddenValue); + updateHiddenAttachments(action.reportActionID, isHiddenValue); }, - [props.action.reportActionID, props.action.message, updateHiddenAttachments], + [action.reportActionID, action.message, updateHiddenAttachments], ); useEffect( () => () => { // ReportActionContextMenu, EmojiPicker and PopoverReactionList are global components, // we should also hide them when the current component is destroyed - if (ReportActionContextMenu.isActiveReportAction(props.action.reportActionID)) { + if (ReportActionContextMenu.isActiveReportAction(action.reportActionID)) { ReportActionContextMenu.hideContextMenu(); ReportActionContextMenu.hideDeleteModal(); } - if (EmojiPickerAction.isActive(props.action.reportActionID)) { + if (EmojiPickerAction.isActive(action.reportActionID)) { EmojiPickerAction.hideEmojiPicker(true); } - if (reactionListRef.current && reactionListRef.current.isActiveReportAction(props.action.reportActionID)) { - reactionListRef.current.hideReactionList(); + if (reactionListRef?.current?.isActiveReportAction(action.reportActionID)) { + reactionListRef?.current?.hideReactionList(); } }, - [props.action.reportActionID, reactionListRef], + [action.reportActionID, reactionListRef], ); useEffect(() => { // We need to hide EmojiPicker when this is a deleted parent action - if (!isDeletedParentAction || !EmojiPickerAction.isActive(props.action.reportActionID)) { + if (!isDeletedParentAction || !EmojiPickerAction.isActive(action.reportActionID)) { return; } EmojiPickerAction.hideEmojiPicker(true); - }, [isDeletedParentAction, props.action.reportActionID]); + }, [isDeletedParentAction, action.reportActionID]); useEffect(() => { - if (!_.isUndefined(prevDraftMessage) || _.isUndefined(props.draftMessage)) { + if (!!prevDraftMessage || !draftMessage) { return; } focusTextInputAfterAnimation(textInputRef.current, 100); - }, [prevDraftMessage, props.draftMessage]); + }, [prevDraftMessage, draftMessage]); useEffect(() => { if (!Permissions.canUseLinkPreviews()) { return; } - const urls = ReportActionsUtils.extractLinksFromMessageHtml(props.action); - if (_.isEqual(downloadedPreviews.current, urls) || props.action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + const urls = ReportActionsUtils.extractLinksFromMessageHtml(action); + if ( + (downloadedPreviews.current.length === urls.length && downloadedPreviews.current.every((value, arrIndex) => value === urls[arrIndex])) || + action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE + ) { return; } downloadedPreviews.current = urls; - Report.expandURLPreview(props.report.reportID, props.action.reportActionID); - }, [props.action, props.report.reportID]); + Report.expandURLPreview(report.reportID, action.reportActionID); + }, [action, report.reportID]); useEffect(() => { - if (_.isUndefined(props.draftMessage) || !ReportActionsUtils.isDeletedAction(props.action)) { + if (!draftMessage || !ReportActionsUtils.isDeletedAction(action)) { return; } - Report.deleteReportActionDraft(props.report.reportID, props.action); - }, [props.draftMessage, props.action, props.report.reportID]); + Report.deleteReportActionDraft(report.reportID, action); + }, [draftMessage, action, report.reportID]); // Hide the message if it is being moderated for a higher offense, or is hidden by a moderator // Removed messages should not be shown anyway and should not need this flow - const latestDecision = lodashGet(props, ['action', 'message', 0, 'moderationDecision', 'decision'], ''); + const latestDecision = action.message?.[0].moderationDecision?.decision ?? ''; useEffect(() => { - if (props.action.actionName !== CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT) { + if (action.actionName !== CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT) { return; } // Hide reveal message button and show the message if latestDecision is changed to empty - if (_.isEmpty(latestDecision)) { + if (!latestDecision) { setModerationDecision(CONST.MODERATION.MODERATOR_DECISION_APPROVED); setIsHidden(false); return; } setModerationDecision(latestDecision); - if (!_.contains([CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING], latestDecision)) { + if (![CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING].some((item) => item === latestDecision)) { setIsHidden(true); return; } setIsHidden(false); - }, [latestDecision, props.action.actionName]); + }, [latestDecision, action.actionName]); const toggleContextMenuFromActiveReportAction = useCallback(() => { - setIsContextMenuActive(ReportActionContextMenu.isActiveReportAction(props.action.reportActionID)); - }, [props.action.reportActionID]); + setIsContextMenuActive(ReportActionContextMenu.isActiveReportAction(action.reportActionID)); + }, [action.reportActionID]); /** * Show the ReportActionContextMenu modal popover. @@ -264,7 +280,7 @@ function ReportActionItem(props) { const showPopover = useCallback( (event: GestureResponderEvent | MouseEvent) => { // Block menu on the message being Edited or if the report action item has errors - if (!_.isUndefined(props.draftMessage) || !_.isEmpty(props.action.errors)) { + if (!!draftMessage || !isEmptyObject(action.errors)) { return; } @@ -274,143 +290,147 @@ function ReportActionItem(props) { CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, event, selection, - popoverAnchorRef, - props.report.reportID, - props.action.reportActionID, + popoverAnchorRef.current, + report.reportID, + action.reportActionID, originalReportID, - props.draftMessage, + draftMessage ?? '', () => setIsContextMenuActive(true), toggleContextMenuFromActiveReportAction, ReportUtils.isArchivedRoom(originalReport), ReportUtils.chatIncludesChronos(originalReport), ); }, - [props.draftMessage, props.action, props.report.reportID, toggleContextMenuFromActiveReportAction, originalReport, originalReportID], + [draftMessage, action, report.reportID, toggleContextMenuFromActiveReportAction, originalReport, originalReportID], ); const toggleReaction = useCallback( - (emoji) => { - Report.toggleEmojiReaction(props.report.reportID, props.action, emoji, props.emojiReactions); + (emoji: Emoji) => { + Report.toggleEmojiReaction(report.reportID, action, emoji, emojiReactions ?? undefined); }, - [props.report, props.action, props.emojiReactions], + [report, action, emojiReactions], ); const contextValue = useMemo( () => ({ anchor: popoverAnchorRef, - report: props.report, - action: props.action, + report, + action, checkIfContextMenuActive: toggleContextMenuFromActiveReportAction, }), - [props.report, props.action, toggleContextMenuFromActiveReportAction], + [report, action, toggleContextMenuFromActiveReportAction], ); /** * Get the content of ReportActionItem - * @param {Boolean} hovered whether the ReportActionItem is hovered - * @param {Boolean} isWhisper whether the report action is a whisper - * @param {Boolean} hasErrors whether the report action has any errors - * @returns {Object} child component(s) + * @param hovered whether the ReportActionItem is hovered + * @param isWhisper whether the report action is a whisper + * @param hasErrors whether the report action has any errors + * @returns child component(s) */ const renderItemContent = (hovered = false, isWhisper = false, hasErrors = false) => { let children; // Show the MoneyRequestPreview for when request was created, bill was split or money was sent if ( - props.action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && - originalMessage && + action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && + action.originalMessage && // For the pay flow, we only want to show MoneyRequestAction when sending money. When paying, we display a regular system message - (originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT || isSendingMoney) + (action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT || isSendingMoney) ) { // There is no single iouReport for bill splits, so only 1:1 requests require an iouReportID - const iouReportID = originalMessage.IOUReportID ? originalMessage.IOUReportID.toString() : '0'; + const iouReportID = action.originalMessage.IOUReportID ? action.originalMessage.IOUReportID.toString() : '0'; children = ( ); - } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW) { + } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW) { children = ( ); } else if ( - props.action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED || - props.action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED || - props.action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED + action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED || + action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED || + action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED ) { - children = ; - } else if (ReportActionsUtils.isCreatedTaskReportAction(props.action)) { + children = ; + } else if (ReportActionsUtils.isCreatedTaskReportAction(action)) { children = ( + // @ts-expect-error TODO: Remove this once ShowContextMenuContext (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. ); - } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED) { - const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(lodashGet(personalDetails, props.report.ownerAccountID)); - const paymentType = lodashGet(props.action, 'originalMessage.paymentType', ''); + } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED) { + const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails[report.ownerAccountID ?? -1]); + const paymentType = action.originalMessage.paymentType ?? ''; - const isSubmitterOfUnsettledReport = ReportUtils.isCurrentUserSubmitter(props.report.reportID) && !ReportUtils.isSettled(props.report.reportID); + const isSubmitterOfUnsettledReport = ReportUtils.isCurrentUserSubmitter(report.reportID) && !ReportUtils.isSettled(report.reportID); const shouldShowAddCreditBankAccountButton = isSubmitterOfUnsettledReport && !store.hasCreditBankAccount() && paymentType !== CONST.IOU.PAYMENT_TYPE.EXPENSIFY; const shouldShowEnableWalletButton = - isSubmitterOfUnsettledReport && - (_.isEmpty(props.userWallet) || props.userWallet.tierName === CONST.WALLET.TIER_NAME.SILVER) && - paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY; + isSubmitterOfUnsettledReport && (isEmptyObject(userWallet) || userWallet?.tierName === CONST.WALLET.TIER_NAME.SILVER) && paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY; children = ( <> {shouldShowAddCreditBankAccountButton && ( )} ) : ( )} ); } - const numberOfThreadReplies = _.get(props, ['action', 'childVisibleActionCount'], 0); + const numberOfThreadReplies = action.childVisibleActionCount ?? 0; - const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(props.action, props.report.reportID); - const oldestFourAccountIDs = _.map(lodashGet(props.action, 'childOldestFourAccountIDs', '').split(','), (accountID) => Number(accountID)); - const draftMessageRightAlign = !_.isUndefined(props.draftMessage) ? styles.chatItemReactionsDraftRight : {}; + const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(action, report.reportID); + const oldestFourAccountIDs = + action.childOldestFourAccountIDs + ?.split(',') + .map((accountID) => Number(accountID)) + .filter((accountID): accountID is number => typeof accountID === 'number') ?? []; + const draftMessageRightAlign = draftMessage ? styles.chatItemReactionsDraftRight : {}; return ( <> {children} - {Permissions.canUseLinkPreviews() && !isHidden && !_.isEmpty(props.action.linkMetadata) && ( - - !_.isEmpty(item))} /> + {Permissions.canUseLinkPreviews() && !isHidden && (action.linkMetadata?.length ?? 0) > 0 && ( + + !isEmptyObject(item))} /> )} - {!ReportActionsUtils.isMessageDeleted(props.action) && ( + {!ReportActionsUtils.isMessageDeleted(action) && ( { if (Session.isAnonymousUser()) { @@ -524,9 +544,9 @@ function ReportActionItem(props) { {shouldDisplayThreadReplies && ( { const content = renderItemContent(hovered || isContextMenuActive, isWhisper, hasErrors); - if (!_.isUndefined(props.draftMessage)) { + if (draftMessage) { return {content}; } - if (!props.displayAsGroup) { + if (!displayAsGroup) { return ( item === moderationDecision)} > @@ -571,30 +591,32 @@ function ReportActionItem(props) { return {content}; }; - if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { - const parentReportAction = props.parentReportActions[props.report.parentReportActionID]; + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { + const parentReportAction = parentReportActions?.[report.parentReportActionID ?? ''] ?? null; if (ReportActionsUtils.isTransactionThread(parentReportAction)) { return ( + // @ts-expect-error TODO: Remove this once ShowContextMenuContext (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. ); } - if (ReportUtils.isTaskReport(props.report)) { - if (ReportUtils.isCanceledTaskReport(props.report, parentReportAction)) { + if (ReportUtils.isTaskReport(report)) { + if (ReportUtils.isCanceledTaskReport(report, parentReportAction)) { return ( <> - + - ${props.translate('parentReportAction.deletedTask')}`} /> + ${translate('parentReportAction.deletedTask')}`} /> @@ -604,21 +626,22 @@ function ReportActionItem(props) { return ( <> - + ); } - if (ReportUtils.isExpenseReport(props.report) || ReportUtils.isIOUReport(props.report)) { + if (ReportUtils.isExpenseReport(report) || ReportUtils.isIOUReport(report)) { return ( - + ); @@ -626,37 +649,32 @@ function ReportActionItem(props) { return ( ); } - if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) { - return ; + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) { + return ; } - if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.CHRONOSOOOLIST) { + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.CHRONOSOOOLIST) { return ( ); } // For the `pay` IOU action on non-send money flow, we don't want to render anything if `isWaitingOnBankAccount` is true // Otherwise, we will see two system messages informing the payee needs to add a bank account or wallet - if ( - props.action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && - lodashGet(props.report, 'isWaitingOnBankAccount', false) && - originalMessage && - originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && - !isSendingMoney - ) { + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !!report?.isWaitingOnBankAccount && action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && !isSendingMoney) { return null; } - const hasErrors = !_.isEmpty(props.action.errors); - const whisperedToAccountIDs = props.action.whisperedToAccountIDs || []; + const hasErrors = !isEmptyObject(action.errors); + const whisperedToAccountIDs = action.whisperedToAccountIDs ?? []; const isWhisper = whisperedToAccountIDs.length > 0; const isMultipleParticipant = whisperedToAccountIDs.length > 1; const isWhisperOnlyVisibleByUser = isWhisper && ReportUtils.isCurrentUserTheOnlyParticipant(whisperedToAccountIDs); @@ -665,41 +683,44 @@ function ReportActionItem(props) { return ( props.isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + // TODO - Remove this once we have a better way to handle this + onPress={() => {}} + style={[action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ? styles.pointerEventsNone : styles.pointerEventsAuto]} + onPressIn={() => isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} onSecondaryInteraction={showPopover} - preventDefaultContextMenu={_.isUndefined(props.draftMessage) && !hasErrors} + preventDefaultContextMenu={!draftMessage && !hasErrors} withoutFocusOnSecondaryInteraction - accessibilityLabel={props.translate('accessibilityHints.chatMessage')} + accessibilityLabel={translate('accessibilityHints.chatMessage')} + accessible > {(hovered) => ( - {props.shouldDisplayNewMarker && } + {shouldDisplayNewMarker && } - + ReportActions.clearReportActionErrors(props.report.reportID, props.action)} - pendingAction={ - !_.isUndefined(props.draftMessage) ? null : props.action.pendingAction || (props.action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : '') - } - shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(props.action, props.report.reportID)} - errors={props.action.errors} + onClose={() => ReportActions.clearReportActionErrors(report.reportID, action)} + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + pendingAction={draftMessage ? undefined : action.pendingAction || (action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : undefined)} + shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(action, report.reportID)} + errors={action.errors} errorRowStyles={[styles.ml10, styles.mr2]} - needsOffscreenAlphaCompositing={ReportActionsUtils.isMoneyRequestAction(props.action)} + needsOffscreenAlphaCompositing={ReportActionsUtils.isMoneyRequestAction(action)} shouldDisableStrikeThrough > {isWhisper && ( @@ -712,7 +733,7 @@ function ReportActionItem(props) { /> - {props.translate('reportActionContextMenu.onlyVisible')} + {translate('reportActionContextMenu.onlyVisible')}   - + ); } -ReportActionItem.propTypes = propTypes; -ReportActionItem.defaultProps = defaultProps; - export default compose( - withWindowDimensions, - withLocalize, withNetwork(), withBlockedFromConcierge({propName: 'blockedFromConcierge'}), withReportActionsDrafts({ propName: 'draftMessage', transformValue: (drafts, props) => { + console.log({drafts}); const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action); const draftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`; - return lodashGet(drafts, [draftKey, props.action.reportActionID, 'message']); + return drafts?.[draftKey]?.[props.action.reportActionID]?.message; }, }), - withOnyx < ReportActionItemProps, - ReportActionItemOnyxProps > - { - preferredSkinTone: { - key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, - initialValue: CONST.EMOJI_DEFAULT_SKIN_TONE, - }, - iouReport: { - key: ({action}) => { - const iouReportID = ReportActionsUtils.getIOUReportIDFromReportActionPreview(action); - return `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`; - }, - }, - emojiReactions: { - key: ({action}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${action.reportActionID}`, - }, - userWallet: { - key: ONYXKEYS.USER_WALLET, - }, - parentReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID ?? ''}`, - canEvict: false, + withOnyx({ + preferredSkinTone: { + key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, + initialValue: CONST.EMOJI_DEFAULT_SKIN_TONE, + }, + iouReport: { + key: ({action}) => { + const iouReportID = ReportActionsUtils.getIOUReportIDFromReportActionPreview(action); + return `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`; }, }, + emojiReactions: { + key: ({action}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${action.reportActionID}`, + }, + userWallet: { + key: ONYXKEYS.USER_WALLET, + }, + parentReportActions: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID ?? ''}`, + canEvict: false, + }, + }), )( memo( ReportActionItem, @@ -786,17 +801,16 @@ export default compose( prevProps.draftMessage === nextProps.draftMessage && prevProps.isMostRecentIOUReportAction === nextProps.isMostRecentIOUReportAction && prevProps.shouldDisplayNewMarker === nextProps.shouldDisplayNewMarker && - _.isEqual(prevProps.emojiReactions, nextProps.emojiReactions) && - _.isEqual(prevProps.action, nextProps.action) && - _.isEqual(prevProps.iouReport, nextProps.iouReport) && - _.isEqual(prevProps.report.pendingFields, nextProps.report.pendingFields) && - _.isEqual(prevProps.report.isDeletedParentAction, nextProps.report.isDeletedParentAction) && - _.isEqual(prevProps.report.errorFields, nextProps.report.errorFields) && - lodashGet(prevProps.report, 'statusNum') === lodashGet(nextProps.report, 'statusNum') && - lodashGet(prevProps.report, 'stateNum') === lodashGet(nextProps.report, 'stateNum') && - lodashGet(prevProps.report, 'parentReportID') === lodashGet(nextProps.report, 'parentReportID') && - lodashGet(prevProps.report, 'parentReportActionID') === lodashGet(nextProps.report, 'parentReportActionID') && - prevProps.translate === nextProps.translate && + lodashIsEqual(prevProps.emojiReactions, nextProps.emojiReactions) && + lodashIsEqual(prevProps.action, nextProps.action) && + lodashIsEqual(prevProps.iouReport, nextProps.iouReport) && + lodashIsEqual(prevProps.report.pendingFields, nextProps.report.pendingFields) && + lodashIsEqual(prevProps.report.isDeletedParentAction, nextProps.report.isDeletedParentAction) && + lodashIsEqual(prevProps.report.errorFields, nextProps.report.errorFields) && + prevProps.report?.statusNum === nextProps.report?.statusNum && + prevProps.report?.stateNum === nextProps.report?.stateNum && + prevProps.report?.parentReportID === nextProps.report?.parentReportID && + prevProps.report?.parentReportActionID === nextProps.report?.parentReportActionID && // TaskReport's created actions render the TaskView, which updates depending on certain fields in the TaskReport ReportUtils.isTaskReport(prevProps.report) === ReportUtils.isTaskReport(nextProps.report) && prevProps.action.actionName === nextProps.action.actionName && @@ -805,8 +819,8 @@ export default compose( ReportUtils.isCompletedTaskReport(prevProps.report) === ReportUtils.isCompletedTaskReport(nextProps.report) && prevProps.report.managerID === nextProps.report.managerID && prevProps.shouldHideThreadDividerLine === nextProps.shouldHideThreadDividerLine && - lodashGet(prevProps.report, 'total', 0) === lodashGet(nextProps.report, 'total', 0) && - lodashGet(prevProps.report, 'nonReimbursableTotal', 0) === lodashGet(nextProps.report, 'nonReimbursableTotal', 0) && + prevProps.report?.total === nextProps.report?.total && + prevProps.report?.nonReimbursableTotal === nextProps.report?.nonReimbursableTotal && prevProps.linkedReportActionID === nextProps.linkedReportActionID, ), ); diff --git a/src/pages/home/report/ReportActionItemBasicMessage.tsx b/src/pages/home/report/ReportActionItemBasicMessage.tsx index f0097a6dce26..770f55e53665 100644 --- a/src/pages/home/report/ReportActionItemBasicMessage.tsx +++ b/src/pages/home/report/ReportActionItemBasicMessage.tsx @@ -4,7 +4,7 @@ import Text from '@components/Text'; import useThemeStyles from '@hooks/useThemeStyles'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; -type ReportActionItemBasicMessageProps = ChildrenProps & { +type ReportActionItemBasicMessageProps = Partial & { message: string; }; diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 96e3e20e5f03..203449dc8837 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -5,6 +5,7 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager, Keyboard, View} from 'react-native'; import type {NativeSyntheticEvent, TextInput, TextInputFocusEventData, TextInputKeyPressEventData} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; import Composer from '@components/Composer'; import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton'; @@ -60,7 +61,7 @@ type ReportActionItemMessageEditProps = { shouldDisableEmojiPicker?: boolean; /** Stores user's preferred skin tone */ - preferredSkinTone?: number; + preferredSkinTone?: OnyxEntry; }; // native ids diff --git a/src/pages/home/report/ReportActionItemThread.tsx b/src/pages/home/report/ReportActionItemThread.tsx index e38021cf6ec1..8d1cc4ea120d 100644 --- a/src/pages/home/report/ReportActionItemThread.tsx +++ b/src/pages/home/report/ReportActionItemThread.tsx @@ -1,5 +1,6 @@ import React from 'react'; import {Text, View} from 'react-native'; +import type {GestureResponderEvent} from 'react-native'; import MultipleAvatars from '@components/MultipleAvatars'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; import useLocalize from '@hooks/useLocalize'; @@ -25,7 +26,7 @@ type ReportActionItemThreadProps = { isHovered: boolean; /** The function that should be called when the thread is LongPressed or right-clicked */ - onSecondaryInteraction: () => void; + onSecondaryInteraction: (event: GestureResponderEvent | MouseEvent) => void; }; function ReportActionItemThread({numberOfReplies, icons, mostRecentReply, childReportID, isHovered, onSecondaryInteraction}: ReportActionItemThreadProps) { diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index c4e30157bf6f..d22ee1599ee6 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -235,6 +235,11 @@ type OriginalMessageMoved = { }; }; +type OriginalMessageMarkedReimbursed = { + actionName: typeof CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED; + originalMessage: unknown; +}; + type OriginalMessage = | OriginalMessageApproved | OriginalMessageIOU @@ -251,7 +256,8 @@ type OriginalMessage = | OriginalMessageModifiedExpense | OriginalMessageReimbursementQueued | OriginalMessageReimbursementDequeued - | OriginalMessageMoved; + | OriginalMessageMoved + | OriginalMessageMarkedReimbursed; export default OriginalMessage; export type { @@ -266,4 +272,5 @@ export type { OriginalMessageIOU, OriginalMessageCreated, OriginalMessageAddComment, + DecisionName, }; From d3a184a877f8d8df700a81b1bdedad3a137ebf19 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 16 Jan 2024 09:27:56 +0100 Subject: [PATCH 025/583] fix: wip --- src/CONST.ts | 1 + src/pages/home/report/ReportActionItem.tsx | 17 +++++++---------- src/types/onyx/OriginalMessage.ts | 4 +++- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index eb908caebf4b..2182173f1a9c 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -608,6 +608,7 @@ const CONST = { }, ACTIONABLE_MENTION_WHISPER_RESOLUTION: { INVITE: 'invited', + NOTHING: '', }, ARCHIVE_REASON: { DEFAULT: 'default', diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index c68064301c8e..0137764cb9c5 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -17,7 +17,7 @@ import {usePersonalDetails, withBlockedFromConcierge, withNetwork, withReportAct import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; import ReportActionItemEmojiReactions from '@components/Reactions/ReportActionItemEmojiReactions'; import RenderHTML from '@components/RenderHTML'; -import ActionableItemButtons from '@components/ReportActionItem/ActionableItemButtons'; +import ActionableItemButtons, {ActionableItem} from '@components/ReportActionItem/ActionableItemButtons'; import ChronosOOOListActions from '@components/ReportActionItem/ChronosOOOListActions'; import MoneyReportView from '@components/ReportActionItem/MoneyReportView'; import MoneyRequestAction from '@components/ReportActionItem/MoneyRequestAction'; @@ -274,7 +274,7 @@ function ReportActionItem({ /** * Show the ReportActionContextMenu modal popover. * - * @param {Object} [event] - A press event. + * @param [event] - A press event. */ const showPopover = useCallback( (event: GestureResponderEvent | MouseEvent) => { @@ -320,7 +320,7 @@ function ReportActionItem({ [report, action, toggleContextMenuFromActiveReportAction], ); - const actionableItemButtons = useMemo(() => { + const actionableItemButtons: ActionableItem[] = useMemo(() => { if (!(action.actionName === CONST.REPORT.ACTIONS.TYPE.ACTIONABLEMENTIONWHISPER && (!action?.originalMessage.resolution ?? null))) { return []; } @@ -505,12 +505,7 @@ function ReportActionItem({ for example: Invite a user mentioned but not a member of the room https://github.com/Expensify/App/issues/32741 */} - {actionableItemButtons.length > 0 && ( - - )} + {actionableItemButtons.length > 0 && } ) : ( 0; const isMultipleParticipant = whisperedToAccountIDs.length > 1; const isWhisperOnlyVisibleByUser = isWhisper && ReportUtils.isCurrentUserTheOnlyParticipant(whisperedToAccountIDs); - const whisperedToPersonalDetails = isWhisper ? Object.values(personalDetails ?? {}).filter((details) => whisperedToAccountIDs.includes(details?.accountID ?? -1)) : []; + const whisperedToPersonalDetails = isWhisper + ? (Object.values(personalDetails ?? {}).filter((details) => whisperedToAccountIDs.includes(details?.accountID ?? -1)) as OnyxTypes.PersonalDetails[]) + : []; const displayNamesWithTooltips = isWhisper ? ReportUtils.getDisplayNamesWithTooltips(whisperedToPersonalDetails, isMultipleParticipant) : []; return ( ; + }; }; type OriginalMessageReimbursementDequeued = { From f2913446296f41953c7bf9a3ecf545d3d8051977 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 16 Jan 2024 11:27:56 +0100 Subject: [PATCH 026/583] fix: wip --- src/components/OnyxProvider.tsx | 6 ++++-- src/pages/home/report/ReportActionItem.tsx | 21 +++++++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/components/OnyxProvider.tsx b/src/components/OnyxProvider.tsx index 124f3558df90..7f05ec3297d9 100644 --- a/src/components/OnyxProvider.tsx +++ b/src/components/OnyxProvider.tsx @@ -8,8 +8,8 @@ import createOnyxContext from './createOnyxContext'; const [withNetwork, NetworkProvider, NetworkContext] = createOnyxContext(ONYXKEYS.NETWORK); const [withPersonalDetails, PersonalDetailsProvider, , usePersonalDetails] = createOnyxContext(ONYXKEYS.PERSONAL_DETAILS_LIST); 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 [withReportActionsDrafts, ReportActionsDraftsProvider, , useReportActionsDrafts] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS); +const [withBlockedFromConcierge, BlockedFromConciergeProvider, , useBlockedFromConcierge] = createOnyxContext(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE); const [withBetas, BetasProvider, BetasContext] = createOnyxContext(ONYXKEYS.BETAS); const [withReportCommentDrafts, ReportCommentDraftsProvider] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT); const [withPreferredTheme, PreferredThemeProvider, PreferredThemeContext] = createOnyxContext(ONYXKEYS.PREFERRED_THEME); @@ -63,4 +63,6 @@ export { useFrequentlyUsedEmojis, withPreferredEmojiSkinTone, PreferredEmojiSkinToneContext, + useBlockedFromConcierge, + useReportActionsDrafts, }; diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index 0137764cb9c5..6a8836dd966d 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -1,3 +1,4 @@ +import {get} from 'lodash'; import lodashIsEqual from 'lodash/isEqual'; import React, {memo, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import type {GestureResponderEvent, TextInput} from 'react-native'; @@ -13,7 +14,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import InlineSystemMessage from '@components/InlineSystemMessage'; import KYCWall from '@components/KYCWall'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import {usePersonalDetails, withBlockedFromConcierge, withNetwork, withReportActionsDrafts} from '@components/OnyxProvider'; +import {useBlockedFromConcierge, usePersonalDetails, useReportActionsDrafts, withBlockedFromConcierge, withNetwork, withReportActionsDrafts} from '@components/OnyxProvider'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; import ReportActionItemEmojiReactions from '@components/Reactions/ReportActionItemEmojiReactions'; import RenderHTML from '@components/RenderHTML'; @@ -77,6 +78,14 @@ import ReportActionItemSingle from './ReportActionItemSingle'; import ReportActionItemThread from './ReportActionItemThread'; import ReportAttachmentsContext from './ReportAttachmentsContext'; +const getDraftMessage = (drafts: OnyxTypes.ReportActionsDrafts, reportID: string, action: OnyxTypes.ReportAction): string | undefined => { + console.log('getDraftMessage', drafts); + const originalReportID = ReportUtils.getOriginalReportID(reportID, action); + const draftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`; + return drafts?.[draftKey]?.[props.action.reportActionID]?.message; + // return drafts[draftKey]. +}; + type ReportActionItemOnyxProps = { /** Stores user's preferred skin tone */ preferredSkinTone: OnyxEntry; @@ -115,7 +124,7 @@ type ReportActionItemProps = { /** Position index of the report action in the overall report FlatList view */ index: number; - /** Draft message - if this is set the comment is in 'edit' mode */ + // /** Draft message - if this is set the comment is in 'edit' mode */ draftMessage?: string; /** Flag to show, hide the thread divider line */ @@ -123,7 +132,7 @@ type ReportActionItemProps = { linkedReportActionID?: string; - blockedFromConcierge: OnyxTypes.BlockedFromConcierge; + blockedFromConcierge?: boolean; } & ReportActionItemOnyxProps; const isIOUReport = (actionObj: OnyxEntry): actionObj is ReportActionBase & OriginalMessageIOU => actionObj?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU; @@ -148,6 +157,10 @@ function ReportActionItem({ }: ReportActionItemProps) { const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); + const blockedFromConciergeTest = useBlockedFromConcierge(); + const reportActionDrafts = useReportActionsDrafts(); + const draftMessageTest = useMemo(() => getDraftMessage(reportActionDrafts, report.reportID, action), [action, report.reportID, reportActionDrafts]); + console.log({blockedFromConciergeTest, draftMessageTest, blockedFromConcierge, draftMessage}); const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -783,13 +796,13 @@ function ReportActionItem({ } export default compose( - withNetwork(), withBlockedFromConcierge({propName: 'blockedFromConcierge'}), withReportActionsDrafts({ propName: 'draftMessage', transformValue: (drafts, props) => { const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action); const draftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`; + console.log({drafts}); return drafts?.[draftKey]?.[props.action.reportActionID]?.message; }, }), From f8b01bf169389cc8fc4a25bc9c5e81059d103571 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 16 Jan 2024 11:59:40 +0100 Subject: [PATCH 027/583] fix: wip --- src/pages/home/report/ReportActionItem.tsx | 90 +++++++++------------- 1 file changed, 37 insertions(+), 53 deletions(-) diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index 6a8836dd966d..fcec76ca6a9d 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -1,4 +1,3 @@ -import {get} from 'lodash'; import lodashIsEqual from 'lodash/isEqual'; import React, {memo, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import type {GestureResponderEvent, TextInput} from 'react-native'; @@ -14,11 +13,12 @@ import * as Expensicons from '@components/Icon/Expensicons'; import InlineSystemMessage from '@components/InlineSystemMessage'; import KYCWall from '@components/KYCWall'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import {useBlockedFromConcierge, usePersonalDetails, useReportActionsDrafts, withBlockedFromConcierge, withNetwork, withReportActionsDrafts} from '@components/OnyxProvider'; +import {useBlockedFromConcierge, usePersonalDetails, useReportActionsDrafts, withBlockedFromConcierge, withReportActionsDrafts} from '@components/OnyxProvider'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; import ReportActionItemEmojiReactions from '@components/Reactions/ReportActionItemEmojiReactions'; import RenderHTML from '@components/RenderHTML'; -import ActionableItemButtons, {ActionableItem} from '@components/ReportActionItem/ActionableItemButtons'; +import type {ActionableItem} from '@components/ReportActionItem/ActionableItemButtons'; +import ActionableItemButtons from '@components/ReportActionItem/ActionableItemButtons'; import ChronosOOOListActions from '@components/ReportActionItem/ChronosOOOListActions'; import MoneyReportView from '@components/ReportActionItem/MoneyReportView'; import MoneyRequestAction from '@components/ReportActionItem/MoneyRequestAction'; @@ -37,7 +37,6 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import compose from '@libs/compose'; import ControlSelection from '@libs/ControlSelection'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import focusTextInputAfterAnimation from '@libs/focusTextInputAfterAnimation'; @@ -79,11 +78,9 @@ import ReportActionItemThread from './ReportActionItemThread'; import ReportAttachmentsContext from './ReportAttachmentsContext'; const getDraftMessage = (drafts: OnyxTypes.ReportActionsDrafts, reportID: string, action: OnyxTypes.ReportAction): string | undefined => { - console.log('getDraftMessage', drafts); const originalReportID = ReportUtils.getOriginalReportID(reportID, action); const draftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`; - return drafts?.[draftKey]?.[props.action.reportActionID]?.message; - // return drafts[draftKey]. + return drafts?.[draftKey]?.[action.reportActionID]?.message; }; type ReportActionItemOnyxProps = { @@ -100,6 +97,8 @@ type ReportActionItemOnyxProps = { /** All the report actions belonging to the report's parent */ parentReportActions: OnyxEntry; + + policyReportFields: OnyxEntry; }; type ReportActionItemProps = { @@ -124,15 +123,10 @@ type ReportActionItemProps = { /** Position index of the report action in the overall report FlatList view */ index: number; - // /** Draft message - if this is set the comment is in 'edit' mode */ - draftMessage?: string; - /** Flag to show, hide the thread divider line */ shouldHideThreadDividerLine?: boolean; linkedReportActionID?: string; - - blockedFromConcierge?: boolean; } & ReportActionItemOnyxProps; const isIOUReport = (actionObj: OnyxEntry): actionObj is ReportActionBase & OriginalMessageIOU => actionObj?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU; @@ -140,7 +134,6 @@ const isIOUReport = (actionObj: OnyxEntry): actionObj is function ReportActionItem({ action, report, - draftMessage = undefined, linkedReportActionID, displayAsGroup, emojiReactions, @@ -153,14 +146,13 @@ function ReportActionItem({ userWallet, shouldHideThreadDividerLine = false, shouldShowSubscriptAvatar = false, - blockedFromConcierge, + policyReportFields, }: ReportActionItemProps) { const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); - const blockedFromConciergeTest = useBlockedFromConcierge(); + const blockedFromConcierge = useBlockedFromConcierge(); const reportActionDrafts = useReportActionsDrafts(); - const draftMessageTest = useMemo(() => getDraftMessage(reportActionDrafts, report.reportID, action), [action, report.reportID, reportActionDrafts]); - console.log({blockedFromConciergeTest, draftMessageTest, blockedFromConcierge, draftMessage}); + const draftMessage = useMemo(() => getDraftMessage(reportActionDrafts, report.reportID, action), [action, report.reportID, reportActionDrafts]); const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -249,7 +241,8 @@ function ReportActionItem({ downloadedPreviews.current = urls; Report.expandURLPreview(report.reportID, action.reportActionID); }, [action, report.reportID]); - + console.log({policyReportFields}); + console.log(action.actionName, action.originalMessage); useEffect(() => { if (!draftMessage || !ReportActionsUtils.isDeletedAction(action)) { return; @@ -411,7 +404,7 @@ function ReportActionItem({ // @ts-expect-error TODO: Remove this once ShowContextMenuContext (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. @@ -795,45 +789,35 @@ function ReportActionItem({ ); } -export default compose( - withBlockedFromConcierge({propName: 'blockedFromConcierge'}), - withReportActionsDrafts({ - propName: 'draftMessage', - transformValue: (drafts, props) => { - const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action); - const draftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`; - console.log({drafts}); - return drafts?.[draftKey]?.[props.action.reportActionID]?.message; - }, - }), - withOnyx({ - preferredSkinTone: { - key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, - initialValue: CONST.EMOJI_DEFAULT_SKIN_TONE, - }, - iouReport: { - key: ({action}) => { - const iouReportID = ReportActionsUtils.getIOUReportIDFromReportActionPreview(action); - return `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`; - }, - }, - emojiReactions: { - key: ({action}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${action.reportActionID}`, - }, - userWallet: { - key: ONYXKEYS.USER_WALLET, - }, - parentReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID ?? ''}`, - canEvict: false, +export default withOnyx({ + preferredSkinTone: { + key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, + initialValue: CONST.EMOJI_DEFAULT_SKIN_TONE, + }, + iouReport: { + key: ({action}) => { + const iouReportID = ReportActionsUtils.getIOUReportIDFromReportActionPreview(action); + return `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`; }, - }), -)( + }, + policyReportFields: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${report.policyID}`, + }, + emojiReactions: { + key: ({action}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${action.reportActionID}`, + }, + userWallet: { + key: ONYXKEYS.USER_WALLET, + }, + parentReportActions: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID ?? ''}`, + canEvict: false, + }, +})( memo( ReportActionItem, (prevProps, nextProps) => prevProps.displayAsGroup === nextProps.displayAsGroup && - prevProps.draftMessage === nextProps.draftMessage && prevProps.isMostRecentIOUReportAction === nextProps.isMostRecentIOUReportAction && prevProps.shouldDisplayNewMarker === nextProps.shouldDisplayNewMarker && lodashIsEqual(prevProps.emojiReactions, nextProps.emojiReactions) && From c4df84dd985d224549762de2cf01ac9392390e5d Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 16 Jan 2024 15:52:59 +0100 Subject: [PATCH 028/583] fix: last types problems --- src/ONYXKEYS.ts | 3 ++- src/libs/EmojiUtils.ts | 16 +++++++++------ .../index.android.ts | 2 +- src/pages/home/report/ReportActionItem.tsx | 20 +++++++++++-------- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 98e3856f4544..ba9954ad2eb9 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -2,6 +2,7 @@ import type {OnyxEntry} from 'react-native-onyx/lib/types'; import type {ValueOf} from 'type-fest'; import type CONST from './CONST'; import type * as OnyxTypes from './types/onyx'; +import type {PolicyReportFields} from './types/onyx/PolicyReportField'; import type DeepValueOf from './types/utils/DeepValueOf'; /** @@ -442,7 +443,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMembers; [ONYXKEYS.COLLECTION.POLICY_MEMBERS_DRAFTS]: OnyxTypes.PolicyMember; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories; - [ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS]: OnyxTypes.PolicyReportField; + [ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS]: PolicyReportFields; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_REPORT_FIELDS]: OnyxTypes.RecentlyUsedReportFields; [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMembers; [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: Record; diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index e34fa0b90fc6..552d4470e9c9 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -243,9 +243,13 @@ function getFrequentlyUsedEmojis(newEmoji: Emoji | Emoji[]): FrequentlyUsedEmoji /** * Given an emoji item object, return an emoji code based on its type. */ -const getEmojiCodeWithSkinColor = (item: Emoji, preferredSkinToneIndex: number): string => { +const getEmojiCodeWithSkinColor = (item: Emoji, preferredSkinToneIndex: OnyxEntry): string | undefined => { const {code, types} = item; - if (types?.[preferredSkinToneIndex]) { + if (!preferredSkinToneIndex) { + return; + } + + if (typeof preferredSkinToneIndex === 'number' && types?.[preferredSkinToneIndex]) { return types[preferredSkinToneIndex]; } @@ -306,7 +310,7 @@ function getAddedEmojis(currentEmojis: Emoji[], formerEmojis: Emoji[]): Emoji[] * Replace any emoji name in a text with the emoji icon. * If we're on mobile, we also add a space after the emoji granted there's no text after it. */ -function replaceEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji { +function replaceEmojis(text: string, preferredSkinTone: OnyxEntry = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji { // emojisTrie is importing the emoji JSON file on the app starting and we want to avoid it const emojisTrie = require('./EmojiTrie').default; @@ -346,9 +350,9 @@ function replaceEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEF // Set the cursor to the end of the last replaced Emoji. Note that we position after // the extra space, if we added one. - cursorPosition = newText.indexOf(emoji) + emojiReplacement.length; + cursorPosition = newText.indexOf(emoji) + (emojiReplacement?.length ?? 0); - newText = newText.replace(emoji, emojiReplacement); + newText = newText.replace(emoji, emojiReplacement ?? ''); } } @@ -370,7 +374,7 @@ function replaceEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEF /** * Find all emojis in a text and replace them with their code. */ -function replaceAndExtractEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji { +function replaceAndExtractEmojis(text: string, preferredSkinTone: OnyxEntry = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji { const {text: convertedText = '', emojis = [], cursorPosition} = replaceEmojis(text, preferredSkinTone, lang); return { diff --git a/src/libs/focusTextInputAfterAnimation/index.android.ts b/src/libs/focusTextInputAfterAnimation/index.android.ts index 31c748f5daa4..cca8a6588103 100644 --- a/src/libs/focusTextInputAfterAnimation/index.android.ts +++ b/src/libs/focusTextInputAfterAnimation/index.android.ts @@ -19,7 +19,7 @@ import type FocusTextInputAfterAnimation from './types'; */ const focusTextInputAfterAnimation: FocusTextInputAfterAnimation = (inputRef, animationLength = 0) => { setTimeout(() => { - inputRef.focus(); + inputRef?.focus(); }, animationLength); }; diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index 2154942cf88a..8f9bb2f47771 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -2,7 +2,7 @@ import lodashIsEqual from 'lodash/isEqual'; import React, {memo, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import type {GestureResponderEvent, TextInput} from 'react-native'; import {InteractionManager, View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; import Button from '@components/Button'; @@ -13,7 +13,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import InlineSystemMessage from '@components/InlineSystemMessage'; import KYCWall from '@components/KYCWall'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import {useBlockedFromConcierge, usePersonalDetails, useReportActionsDrafts, withBlockedFromConcierge, withReportActionsDrafts} from '@components/OnyxProvider'; +import {useBlockedFromConcierge, usePersonalDetails, useReportActionsDrafts} from '@components/OnyxProvider'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; import ReportActionItemEmojiReactions from '@components/Reactions/ReportActionItemEmojiReactions'; import RenderHTML from '@components/RenderHTML'; @@ -60,6 +60,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {DecisionName, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; +import type {PolicyReportFields} from '@src/types/onyx/PolicyReportField'; import type {ReportActionBase} from '@src/types/onyx/ReportAction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; @@ -77,10 +78,11 @@ import ReportActionItemSingle from './ReportActionItemSingle'; import ReportActionItemThread from './ReportActionItemThread'; import ReportAttachmentsContext from './ReportAttachmentsContext'; -const getDraftMessage = (drafts: OnyxTypes.ReportActionsDrafts, reportID: string, action: OnyxTypes.ReportAction): string | undefined => { +const getDraftMessage = (drafts: OnyxCollection, reportID: string, action: OnyxTypes.ReportAction): string | undefined => { const originalReportID = ReportUtils.getOriginalReportID(reportID, action); const draftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`; - return drafts?.[draftKey]?.[action.reportActionID]?.message; + const draftMessage = drafts?.[draftKey]?.[action.reportActionID]; + return typeof draftMessage === 'string' ? draftMessage : draftMessage?.message; }; type ReportActionItemOnyxProps = { @@ -98,7 +100,8 @@ type ReportActionItemOnyxProps = { /** All the report actions belonging to the report's parent */ parentReportActions: OnyxEntry; - policyReportFields: OnyxEntry; + /** All policy report fields */ + policyReportFields: OnyxEntry; }; type ReportActionItemProps = { @@ -151,7 +154,8 @@ function ReportActionItem({ const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); const blockedFromConcierge = useBlockedFromConcierge(); - const reportActionDrafts = useReportActionsDrafts(); + // TODO need to fix createOnyxContext to report types as OnyxCollection if provided key is collection + const reportActionDrafts = useReportActionsDrafts() as OnyxCollection; const draftMessage = useMemo(() => getDraftMessage(reportActionDrafts, report.reportID, action), [action, report.reportID, reportActionDrafts]); const theme = useTheme(); const styles = useThemeStyles(); @@ -241,8 +245,7 @@ function ReportActionItem({ downloadedPreviews.current = urls; Report.expandURLPreview(report.reportID, action.reportActionID); }, [action, report.reportID]); - console.log({policyReportFields}); - console.log(action.actionName, action.originalMessage); + useEffect(() => { if (!draftMessage || !ReportActionsUtils.isDeletedAction(action)) { return; @@ -783,6 +786,7 @@ function ReportActionItem({ )} + {/* @ts-expect-error TODO check if there is a field on the reportAction object */} From 8b88e303eafda0b842ab6e13df4f13dee6886bcf Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 16 Jan 2024 16:26:08 +0100 Subject: [PATCH 029/583] fix: remove unnecessary comment --- src/pages/home/report/ReportActionItem.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index 8f9bb2f47771..6fd1803d1417 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -390,7 +390,6 @@ function ReportActionItem({ containerStyles={displayAsGroup ? [] : [styles.mt2]} action={action} isHovered={hovered} - // TODO: Check if passing .current is correct contextMenuAnchor={popoverAnchorRef} checkIfContextMenuActive={toggleContextMenuFromActiveReportAction} isWhisper={isWhisper} From 62c72a7922230516990e93161998a47b9754cdd4 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 17 Jan 2024 09:16:17 +0100 Subject: [PATCH 030/583] fix: typecheck --- src/components/ReportActionItem/TaskPreview.tsx | 2 +- src/components/ShowContextMenuContext.ts | 4 ++-- .../report/ContextMenu/MiniReportActionContextMenu/types.ts | 2 +- src/pages/home/report/ReportActionItem.tsx | 4 +--- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index 8ef837ed986d..99bdf58d40e1 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -65,7 +65,7 @@ type TaskPreviewProps = WithCurrentUserPersonalDetailsProps & chatReportID: string; /** Popover context menu anchor, used for showing context menu */ - contextMenuAnchor: RNText | null; + contextMenuAnchor: RNText | View | null; /** Callback for updating context menu active state, used for showing context menu */ checkIfContextMenuActive: () => void; diff --git a/src/components/ShowContextMenuContext.ts b/src/components/ShowContextMenuContext.ts index 17557051bef9..c0179445b1ff 100644 --- a/src/components/ShowContextMenuContext.ts +++ b/src/components/ShowContextMenuContext.ts @@ -1,6 +1,6 @@ import {createContext} from 'react'; // eslint-disable-next-line no-restricted-imports -import type {GestureResponderEvent, Text as RNText} from 'react-native'; +import type {GestureResponderEvent, Text as RNText, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as ReportUtils from '@libs/ReportUtils'; @@ -36,7 +36,7 @@ ShowContextMenuContext.displayName = 'ShowContextMenuContext'; */ function showContextMenuForReport( event: GestureResponderEvent | MouseEvent, - anchor: RNText | null, + anchor: RNText | View | null, reportID: string, action: OnyxEntry, checkIfContextMenuActive: () => void, diff --git a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts index 98b38dcb6968..b7c3d6214094 100644 --- a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts +++ b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts @@ -1,6 +1,6 @@ import type {BaseReportActionContextMenuProps} from '@pages/home/report/ContextMenu/BaseReportActionContextMenu'; -type MiniReportActionContextMenuProps = Omit & { +type MiniReportActionContextMenuProps = Omit & { /** Should the reportAction this menu is attached to have the appearance of being grouped with the previous reportAction? */ displayAsGroup?: boolean; }; diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index 6fd1803d1417..c29859512e20 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -314,7 +314,7 @@ function ReportActionItem({ const toggleReaction = useCallback( (emoji: Emoji) => { - Report.toggleEmojiReaction(report.reportID, action, emoji, emojiReactions ?? undefined); + Report.toggleEmojiReaction(report.reportID, action, emoji, emojiReactions); }, [report, action, emojiReactions], ); @@ -411,7 +411,6 @@ function ReportActionItem({ policyID={ReportUtils.getRootParentReport(report)?.policyID ?? ''} action={action} isHovered={hovered} - // TODO: Check if passing .current is correct contextMenuAnchor={popoverAnchorRef.current} checkIfContextMenuActive={toggleContextMenuFromActiveReportAction} /> @@ -737,7 +736,6 @@ function ReportActionItem({ reportID={report.reportID} reportActionID={action.reportActionID} originalReportID={originalReportID ?? ''} - // @ts-expect-error TODO: Remove this once TaskView (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. isArchivedRoom={ReportUtils.isArchivedRoom(report)} displayAsGroup={displayAsGroup} isVisible={hovered && !draftMessage && !hasErrors} From e1deff2cc31d673be9814f81bdf6c0df78783a80 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 17 Jan 2024 11:28:18 +0100 Subject: [PATCH 031/583] fix: resolve comments --- src/libs/isReportMessageAttachment.ts | 6 +-- src/pages/home/report/ReportActionItem.tsx | 61 +++++++++++----------- src/types/onyx/index.ts | 8 ++- 3 files changed, 40 insertions(+), 35 deletions(-) diff --git a/src/libs/isReportMessageAttachment.ts b/src/libs/isReportMessageAttachment.ts index 5ff4bfbef093..460492c0e460 100644 --- a/src/libs/isReportMessageAttachment.ts +++ b/src/libs/isReportMessageAttachment.ts @@ -8,14 +8,14 @@ import type {Message} from '@src/types/onyx/ReportAction'; * @param reportActionMessage report action's message as text, html and translationKey */ export default function isReportMessageAttachment(message: Message | undefined): boolean { - if (!message?.text || !message?.html) { + if (!message?.text || !message.html) { return false; } - if (message?.translationKey && message?.text === CONST.ATTACHMENT_MESSAGE_TEXT) { + if (message.translationKey && message.text === CONST.ATTACHMENT_MESSAGE_TEXT) { return message?.translationKey === CONST.TRANSLATION_KEYS.ATTACHMENT; } const regex = new RegExp(` ${CONST.ATTACHMENT_SOURCE_ATTRIBUTE}="(.*)"`, 'i'); - return message?.text === CONST.ATTACHMENT_MESSAGE_TEXT && (!!message?.html.match(regex) || message?.html === CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML); + return message.text === CONST.ATTACHMENT_MESSAGE_TEXT && (!!message.html.match(regex) || message.html === CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML); } diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index c29859512e20..b18e78a120b6 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -59,9 +59,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; -import type {DecisionName, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; -import type {PolicyReportFields} from '@src/types/onyx/PolicyReportField'; -import type {ReportActionBase} from '@src/types/onyx/ReportAction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; import MiniReportActionContextMenu from './ContextMenu/MiniReportActionContextMenu'; @@ -101,7 +98,7 @@ type ReportActionItemOnyxProps = { parentReportActions: OnyxEntry; /** All policy report fields */ - policyReportFields: OnyxEntry; + policyReportFields: OnyxEntry; }; type ReportActionItemProps = { @@ -132,7 +129,8 @@ type ReportActionItemProps = { linkedReportActionID?: string; } & ReportActionItemOnyxProps; -const isIOUReport = (actionObj: OnyxEntry): actionObj is ReportActionBase & OriginalMessageIOU => actionObj?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU; +const isIOUReport = (actionObj: OnyxEntry): actionObj is OnyxTypes.ReportActionBase & OnyxTypes.OriginalMessageIOU => + actionObj?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU; function ReportActionItem({ action, @@ -163,7 +161,7 @@ function ReportActionItem({ const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; const [isContextMenuActive, setIsContextMenuActive] = useState(() => ReportActionContextMenu.isActiveReportAction(action.reportActionID)); const [isHidden, setIsHidden] = useState(false); - const [moderationDecision, setModerationDecision] = useState(CONST.MODERATION.MODERATOR_DECISION_APPROVED); + const [moderationDecision, setModerationDecision] = useState(CONST.MODERATION.MODERATOR_DECISION_APPROVED); const reactionListRef = useContext(ReactionListContext); const {updateHiddenAttachments} = useContext(ReportAttachmentsContext); const textInputRef = useRef(); @@ -185,7 +183,7 @@ function ReportActionItem({ const updateHiddenState = useCallback( (isHiddenValue: boolean) => { setIsHidden(isHiddenValue); - const isAttachment = ReportUtils.isReportMessageAttachment(action.message?.[action.message?.length - 1]); + const isAttachment = ReportUtils.isReportMessageAttachment(action.message?.at(-1)); if (!isAttachment) { return; } @@ -222,7 +220,7 @@ function ReportActionItem({ }, [isDeletedParentAction, action.reportActionID]); useEffect(() => { - if (!!prevDraftMessage || !draftMessage) { + if (prevDraftMessage !== undefined || draftMessage === undefined) { return; } @@ -235,10 +233,7 @@ function ReportActionItem({ } const urls = ReportActionsUtils.extractLinksFromMessageHtml(action); - if ( - (downloadedPreviews.current.length === urls.length && downloadedPreviews.current.every((value, arrIndex) => value === urls[arrIndex])) || - action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE - ) { + if (lodashIsEqual(downloadedPreviews.current, urls) || action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { return; } @@ -247,7 +242,7 @@ function ReportActionItem({ }, [action, report.reportID]); useEffect(() => { - if (!draftMessage || !ReportActionsUtils.isDeletedAction(action)) { + if (draftMessage === undefined || !ReportActionsUtils.isDeletedAction(action)) { return; } Report.deleteReportActionDraft(report.reportID, action); @@ -288,7 +283,7 @@ function ReportActionItem({ const showPopover = useCallback( (event: GestureResponderEvent | MouseEvent) => { // Block menu on the message being Edited or if the report action item has errors - if (!!draftMessage || !isEmptyObject(action.errors)) { + if (draftMessage !== undefined || !isEmptyObject(action.errors)) { return; } @@ -355,12 +350,12 @@ function ReportActionItem({ * @param hasErrors whether the report action has any errors * @returns child component(s) */ - const renderItemContent = (hovered = false, isWhisper = false, hasErrors = false) => { + const renderItemContent = (hovered = false, isWhisper = false, hasErrors = false): React.JSX.Element => { let children; // Show the MoneyRequestPreview for when request was created, bill was split or money was sent if ( - action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && + isIOUReport(action) && action.originalMessage && // For the pay flow, we only want to show MoneyRequestAction when sending money. When paying, we display a regular system message (action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT || isSendingMoney) @@ -475,7 +470,7 @@ function ReportActionItem({ children = ( // @ts-expect-error TODO: Remove this once ShowContextMenuContext (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. - {!draftMessage ? ( + {draftMessage === undefined ? ( Number(accountID)) .filter((accountID): accountID is number => typeof accountID === 'number') ?? []; - const draftMessageRightAlign = draftMessage ? styles.chatItemReactionsDraftRight : {}; + const draftMessageRightAlign = draftMessage !== undefined ? styles.chatItemReactionsDraftRight : {}; return ( <> {children} {Permissions.canUseLinkPreviews() && !isHidden && (action.linkMetadata?.length ?? 0) > 0 && ( - + !isEmptyObject(item))} /> )} @@ -592,10 +587,10 @@ function ReportActionItem({ * @param hasErrors whether the report action has any errors * @returns report action item */ - const renderReportActionItem = (hovered: boolean, isWhisper: boolean, hasErrors: boolean) => { + const renderReportActionItem = (hovered: boolean, isWhisper: boolean, hasErrors: boolean): React.JSX.Element => { const content = renderItemContent(hovered || isContextMenuActive, isWhisper, hasErrors); - if (draftMessage) { + if (draftMessage !== undefined) { return {content}; } @@ -603,7 +598,7 @@ function ReportActionItem({ return ( ${translate('parentReportAction.deletedTask')}`} /> @@ -698,7 +693,7 @@ function ReportActionItem({ // For the `pay` IOU action on non-send money flow, we don't want to render anything if `isWaitingOnBankAccount` is true // Otherwise, we will see two system messages informing the payee needs to add a bank account or wallet - if (action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !!report?.isWaitingOnBankAccount && action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && !isSendingMoney) { + if (isIOUReport(action) && !!report?.isWaitingOnBankAccount && action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && !isSendingMoney) { return null; } @@ -720,14 +715,14 @@ function ReportActionItem({ onPressIn={() => isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} onSecondaryInteraction={showPopover} - preventDefaultContextMenu={!draftMessage && !hasErrors} + preventDefaultContextMenu={draftMessage === undefined && !hasErrors} withoutFocusOnSecondaryInteraction accessibilityLabel={translate('accessibilityHints.chatMessage')} accessible > {(hovered) => ( @@ -738,15 +733,17 @@ function ReportActionItem({ originalReportID={originalReportID ?? ''} isArchivedRoom={ReportUtils.isArchivedRoom(report)} displayAsGroup={displayAsGroup} - isVisible={hovered && !draftMessage && !hasErrors} + isVisible={hovered && draftMessage === undefined && !hasErrors} draftMessage={draftMessage} isChronosReport={ReportUtils.chatIncludesChronos(originalReport)} /> - + ReportActions.clearReportActionErrors(report.reportID, action)} // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - pendingAction={draftMessage ? undefined : action.pendingAction || (action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : undefined)} + pendingAction={ + draftMessage !== undefined ? undefined : action.pendingAction || (action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : undefined) + } shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(action, report.reportID)} errors={action.errors} errorRowStyles={[styles.ml10, styles.mr2]} @@ -811,7 +808,7 @@ export default withOnyx({ key: ONYXKEYS.USER_WALLET, }, parentReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID ?? ''}`, + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID ?? '0'}`, canEvict: false, }, })( @@ -841,6 +838,8 @@ export default withOnyx({ prevProps.shouldHideThreadDividerLine === nextProps.shouldHideThreadDividerLine && prevProps.report?.total === nextProps.report?.total && prevProps.report?.nonReimbursableTotal === nextProps.report?.nonReimbursableTotal && - prevProps.linkedReportActionID === nextProps.linkedReportActionID, + prevProps.linkedReportActionID === nextProps.linkedReportActionID && + lodashIsEqual(prevProps.policyReportFields, nextProps.policyReportFields) && + lodashIsEqual(prevProps.report.reportFields, nextProps.report.reportFields), ), ); diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 8cba351d0f45..69ec028e2c14 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -22,6 +22,7 @@ import type MapboxAccessToken from './MapboxAccessToken'; import type Modal from './Modal'; import type Network from './Network'; import type {OnyxUpdateEvent, OnyxUpdatesFromServer} from './OnyxUpdatesFromServer'; +import type {DecisionName, OriginalMessageIOU} from './OriginalMessage'; import type PersonalBankAccount from './PersonalBankAccount'; import type {PersonalDetailsList} from './PersonalDetails'; import type PersonalDetails from './PersonalDetails'; @@ -31,6 +32,7 @@ import type {PolicyCategories, PolicyCategory} from './PolicyCategory'; import type {PolicyMembers} from './PolicyMember'; import type PolicyMember from './PolicyMember'; import type PolicyReportField from './PolicyReportField'; +import type {PolicyReportFields} from './PolicyReportField'; import type {PolicyTag, PolicyTags} from './PolicyTag'; import type PrivatePersonalDetails from './PrivatePersonalDetails'; import type RecentlyUsedCategories from './RecentlyUsedCategories'; @@ -40,7 +42,7 @@ import type RecentWaypoint from './RecentWaypoint'; import type ReimbursementAccount from './ReimbursementAccount'; import type ReimbursementAccountDraft from './ReimbursementAccountDraft'; import type Report from './Report'; -import type {ReportActions} from './ReportAction'; +import type {ReportActionBase, ReportActions} from './ReportAction'; import type ReportAction from './ReportAction'; import type ReportActionReactions from './ReportActionReactions'; import type ReportActionsDraft from './ReportActionsDraft'; @@ -138,5 +140,9 @@ export type { WalletTransfer, ReportUserIsTyping, PolicyReportField, + PolicyReportFields, RecentlyUsedReportFields, + DecisionName, + OriginalMessageIOU, + ReportActionBase, }; From d54eef9e335cf7e32fe629ecf43607c7244923d0 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 17 Jan 2024 14:32:30 +0100 Subject: [PATCH 032/583] fix: types in OnyxKeys and migration files --- src/ONYXKEYS.ts | 2 +- src/libs/migrations/KeyReportActionsDraftByReportActionID.ts | 3 ++- src/libs/migrations/RemoveEmptyReportActionsDrafts.ts | 4 ++-- src/pages/home/report/ReportActionItem.tsx | 5 ++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index ba9954ad2eb9..116a58df039a 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -450,7 +450,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.REPORT]: OnyxTypes.Report; [ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata; [ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportActions; - [ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS]: OnyxTypes.ReportActionsDrafts; + [ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS]: Record; [ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS]: OnyxTypes.ReportActionReactions; [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT]: string; [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT_NUMBER_OF_LINES]: number; diff --git a/src/libs/migrations/KeyReportActionsDraftByReportActionID.ts b/src/libs/migrations/KeyReportActionsDraftByReportActionID.ts index dbf2829a6c28..e098e82dca48 100644 --- a/src/libs/migrations/KeyReportActionsDraftByReportActionID.ts +++ b/src/libs/migrations/KeyReportActionsDraftByReportActionID.ts @@ -27,7 +27,7 @@ export default function () { return resolve(); } - const newReportActionsDrafts: Record> = {}; + const newReportActionsDrafts: Record>> = {}; Object.entries(allReportActionsDrafts).forEach(([onyxKey, reportActionDraft]) => { if (typeof reportActionDraft !== 'string') { return; @@ -47,6 +47,7 @@ export default function () { // If newReportActionsDrafts[newOnyxKey] isn't set, fall back on the migrated draft if there is one const currentActionsDrafts = newReportActionsDrafts[newOnyxKey] ?? allReportActionsDrafts[newOnyxKey]; + newReportActionsDrafts[newOnyxKey] = { ...currentActionsDrafts, [reportActionID]: reportActionDraft, diff --git a/src/libs/migrations/RemoveEmptyReportActionsDrafts.ts b/src/libs/migrations/RemoveEmptyReportActionsDrafts.ts index 48493a82d641..bf3e3067bfaf 100644 --- a/src/libs/migrations/RemoveEmptyReportActionsDrafts.ts +++ b/src/libs/migrations/RemoveEmptyReportActionsDrafts.ts @@ -24,9 +24,9 @@ export default function (): Promise { return resolve(); } - const newReportActionsDrafts: Record> = {}; + const newReportActionsDrafts: Record>> = {}; Object.entries(allReportActionsDrafts).forEach(([onyxKey, reportActionDrafts]) => { - const newReportActionsDraftsForReport: Record = {}; + const newReportActionsDraftsForReport: Record> = {}; // Whether there is at least one draft in this report that has to be migrated let hasUnmigratedDraft = false; diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index b18e78a120b6..f5724e245473 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -152,8 +152,7 @@ function ReportActionItem({ const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); const blockedFromConcierge = useBlockedFromConcierge(); - // TODO need to fix createOnyxContext to report types as OnyxCollection if provided key is collection - const reportActionDrafts = useReportActionsDrafts() as OnyxCollection; + const reportActionDrafts = useReportActionsDrafts(); const draftMessage = useMemo(() => getDraftMessage(reportActionDrafts, report.reportID, action), [action, report.reportID, reportActionDrafts]); const theme = useTheme(); const styles = useThemeStyles(); @@ -742,7 +741,7 @@ function ReportActionItem({ onClose={() => ReportActions.clearReportActionErrors(report.reportID, action)} // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing pendingAction={ - draftMessage !== undefined ? undefined : action.pendingAction || (action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : undefined) + draftMessage !== undefined ? undefined : action.pendingAction ?? (action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : undefined) } shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(action, report.reportID)} errors={action.errors} From e3bd929fd4c585716c2be66c15f8a401173b0ce7 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 18 Jan 2024 11:47:14 +0100 Subject: [PATCH 033/583] fix :resolve conflicts --- src/pages/home/report/ReportActionItem.tsx | 43 ++++++++++------------ 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index 6d057b4cf524..2350e5f79d54 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -434,33 +434,30 @@ function ReportActionItem({ pressOnEnter /> )} - {shouldShowEnableWalletButton && ( - // @ts-expect-error TODO: Remove this once KYCWall (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. - Navigation.navigate(ROUTES.ENABLE_PAYMENTS)} - enablePaymentsRoute={ROUTES.ENABLE_PAYMENTS} - addBankAccountRoute={ROUTES.BANK_ACCOUNT_PERSONAL} - addDebitCardRoute={ROUTES.SETTINGS_ADD_DEBIT_CARD} - chatReportID={report.reportID} - iouReport={iouReport} - > - {/* @ts-expect-error TODO: Remove this once KYCWall (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. */} - {(triggerKYCFlow, buttonRef) => ( -