From 1c13e82db29b037d16d7a8a0b8db8df158182495 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 12 Jan 2024 08:56:46 +0100 Subject: [PATCH 01/31] 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 02/31] 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 03/31] 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 04/31] 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 05/31] 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 06/31] 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 07/31] 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 08/31] 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 09/31] 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 10/31] 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 11/31] 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 12/31] 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 13/31] 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) => ( -