diff --git a/assets/images/arrows-leftright.svg b/assets/images/arrows-leftright.svg new file mode 100644 index 000000000000..53c75d411734 --- /dev/null +++ b/assets/images/arrows-leftright.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/images/chatbubble-slash.svg b/assets/images/chatbubble-slash.svg new file mode 100644 index 000000000000..09d2b5bd3149 --- /dev/null +++ b/assets/images/chatbubble-slash.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/CONST.ts b/src/CONST.ts index ac4b9562672d..f3cf71fef51f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -446,6 +446,9 @@ const CONST = { MAX_LENGTH: 83, }, + REVERSED_TRANSACTION_ATTRIBUTE: 'is-reversed-transaction', + HIDDEN_MESSAGE_ATTRIBUTE: 'is-hidden-message', + CALENDAR_PICKER: { // Numbers were arbitrarily picked. MIN_YEAR: CURRENT_YEAR - 100, diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx index b4002767524f..12b515194928 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx @@ -37,6 +37,11 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim mixedUAStyles: {...styles.formError, ...styles.mb0}, contentModel: HTMLContentModel.block, }), + 'deleted-action': HTMLElementModel.fromCustomModel({ + tagName: 'alert-text', + mixedUAStyles: {...styles.formError, ...styles.mb0}, + contentModel: HTMLContentModel.block, + }), 'muted-text': HTMLElementModel.fromCustomModel({ tagName: 'muted-text', mixedUAStyles: {...styles.colorMuted, ...styles.mb0}, diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/DeletedActionRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/DeletedActionRenderer.tsx new file mode 100644 index 000000000000..4e6334d90ebd --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/DeletedActionRenderer.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-html'; +import {TNodeChildrenRenderer} from 'react-native-render-html'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import Text from '@components/Text'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; + +function DeletedActionRenderer({tnode}: CustomRendererProps) { + const styles = useThemeStyles(); + const theme = useTheme(); + const htmlAttribs = tnode.attributes; + + const reversedTransactionValue = htmlAttribs[CONST.REVERSED_TRANSACTION_ATTRIBUTE]; + const hiddenMessageValue = htmlAttribs[CONST.HIDDEN_MESSAGE_ATTRIBUTE]; + + const getIcon = () => { + if (reversedTransactionValue === 'true') { + return Expensicons.ArrowsLeftRight; + } + if (hiddenMessageValue === 'true') { + return Expensicons.EyeDisabled; + } + return Expensicons.Trashcan; + }; + + return ( + + + { + const firstChild = props?.childTnode?.children?.at(0); + const data = firstChild && 'data' in firstChild ? firstChild.data : null; + + if (typeof data === 'string') { + return {data}; + } + return props.childElement; + }} + /> + + ); +} + +DeletedActionRenderer.displayName = 'DeletedActionRenderer'; + +export default DeletedActionRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts index ce24584048b0..91ed66f8b931 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts @@ -1,6 +1,7 @@ import type {CustomTagRendererRecord} from 'react-native-render-html'; import AnchorRenderer from './AnchorRenderer'; import CodeRenderer from './CodeRenderer'; +import DeletedActionRenderer from './DeletedActionRenderer'; import EditedRenderer from './EditedRenderer'; import EmojiRenderer from './EmojiRenderer'; import ImageRenderer from './ImageRenderer'; @@ -30,6 +31,7 @@ const HTMLEngineProviderComponentList: CustomTagRendererRecord = { 'mention-here': MentionHereRenderer, emoji: EmojiRenderer, 'next-step-email': NextStepEmailRenderer, + 'deleted-action': DeletedActionRenderer, /* eslint-enable @typescript-eslint/naming-convention */ }; diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 02a6843dc11f..e4072504f3d6 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -7,6 +7,7 @@ import ArrowRightLong from '@assets/images/arrow-right-long.svg'; import ArrowRight from '@assets/images/arrow-right.svg'; import ArrowUpLong from '@assets/images/arrow-up-long.svg'; import UpArrow from '@assets/images/arrow-up.svg'; +import ArrowsLeftRight from '@assets/images/arrows-leftright.svg'; import ArrowsUpDown from '@assets/images/arrows-updown.svg'; import AttachmentNotFound from '@assets/images/attachment-not-found.svg'; import AdminRoomAvatar from '@assets/images/avatars/admin-room.svg'; @@ -43,6 +44,7 @@ import Cash from '@assets/images/cash.svg'; import Chair from '@assets/images/chair.svg'; import ChatBubbleAdd from '@assets/images/chatbubble-add.svg'; import ChatBubbleReply from '@assets/images/chatbubble-reply.svg'; +import ChatBubbleSlash from '@assets/images/chatbubble-slash.svg'; import ChatBubbleUnread from '@assets/images/chatbubble-unread.svg'; import ChatBubble from '@assets/images/chatbubble.svg'; import ChatBubbles from '@assets/images/chatbubbles.svg'; @@ -220,6 +222,7 @@ export { ArrowRight, ArrowRightLong, ArrowsUpDown, + ArrowsLeftRight, ArrowUpLong, ArrowDownLong, AttachmentNotFound, @@ -390,6 +393,7 @@ export { Linkedin, Instagram, ChatBubbleAdd, + ChatBubbleSlash, ChatBubbleUnread, ChatBubbleReply, Lightbulb, diff --git a/src/components/ReportActionItem/MoneyRequestAction.tsx b/src/components/ReportActionItem/MoneyRequestAction.tsx index af54e2940d3f..cc64fed1d3e6 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.tsx +++ b/src/components/ReportActionItem/MoneyRequestAction.tsx @@ -1,14 +1,13 @@ import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import RenderHTML from '@components/RenderHTML'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as IOUUtils from '@libs/IOUUtils'; +import {isIOUReportPendingCurrencyConversion} from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import {isDeletedParentAction, isReversedTransaction, isSplitBillAction, isTrackExpenseAction} from '@libs/ReportActionsUtils'; import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -18,18 +17,7 @@ import type * as OnyxTypes from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import MoneyRequestPreview from './MoneyRequestPreview'; -type MoneyRequestActionOnyxProps = { - /** Chat report associated with iouReport */ - chatReport: OnyxEntry; - - /** IOU report data object */ - iouReport: OnyxEntry; - - /** Report actions for this report */ - reportActions: OnyxEntry; -}; - -type MoneyRequestActionProps = MoneyRequestActionOnyxProps & { +type MoneyRequestActionProps = { /** All the data of the action */ action: OnyxTypes.ReportAction; @@ -72,9 +60,6 @@ function MoneyRequestAction({ isMostRecentIOUReportAction, contextMenuAnchor, checkIfContextMenuActive = () => {}, - chatReport, - iouReport, - reportActions, isHovered = false, style, isWhisper = false, @@ -83,23 +68,29 @@ function MoneyRequestAction({ const styles = useThemeStyles(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const isSplitBillAction = ReportActionsUtils.isSplitBillAction(action); - const isTrackExpenseAction = ReportActionsUtils.isTrackExpenseAction(action); + const isActionSplitBill = isSplitBillAction(action); + const isActionTrackExpense = isTrackExpenseAction(action); + const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID || CONST.DEFAULT_NUMBER_ID}`, {canEvict: false}); + const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID || CONST.DEFAULT_NUMBER_ID}`); + const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${requestReportID}`); const onMoneyRequestPreviewPressed = () => { - if (isSplitBillAction) { - const reportActionID = action.reportActionID ?? '-1'; + if (isActionSplitBill) { + const reportActionID = action.reportActionID; Navigation.navigate(ROUTES.SPLIT_BILL_DETAILS.getRoute(chatReportID, reportActionID, Navigation.getReportRHPActiveRoute())); return; } - const childReportID = action?.childReportID ?? '-1'; + const childReportID = action?.childReportID; + if (!childReportID) { + return; + } Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); }; let shouldShowPendingConversionMessage = false; - const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(action); - const isReversedTransaction = ReportActionsUtils.isReversedTransaction(action); + const isParentActionDeleted = isDeletedParentAction(action); + const isTransactionReveresed = isReversedTransaction(action); if ( !isEmptyObject(iouReport) && !isEmptyObject(reportActions) && @@ -108,25 +99,25 @@ function MoneyRequestAction({ action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && isOffline ) { - shouldShowPendingConversionMessage = IOUUtils.isIOUReportPendingCurrencyConversion(iouReport); + shouldShowPendingConversionMessage = isIOUReportPendingCurrencyConversion(iouReport); } - if (isDeletedParentAction || isReversedTransaction) { + if (isParentActionDeleted || isTransactionReveresed) { let message: TranslationPaths; - if (isReversedTransaction) { + if (isTransactionReveresed) { message = 'parentReportAction.reversedTransaction'; } else { message = 'parentReportAction.deletedExpense'; } - return ${translate(message)}`} />; + return ${translate(message)}`} />; } return ( ({ - chatReport: { - key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, - }, - iouReport: { - key: ({requestReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${requestReportID}`, - }, - reportActions: { - key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, - canEvict: false, - }, -})(MoneyRequestAction); +export default MoneyRequestAction; diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index 2ea295d16143..91e9cdbbc9c1 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -20,15 +20,15 @@ import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import {checkIfActionIsAllowed} from '@libs/actions/Session'; +import {canActionTask, completeTask, getTaskAssigneeAccountID, reopenTask} from '@libs/actions/Task'; import ControlSelection from '@libs/ControlSelection'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import getButtonState from '@libs/getButtonState'; import Navigation from '@libs/Navigation/Navigation'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as TaskUtils from '@libs/TaskUtils'; +import {isCanceledTaskReport, isOpenTaskReport, isReportManager} from '@libs/ReportUtils'; +import {getTaskTitleFromReport} from '@libs/TaskUtils'; import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; -import * as Session from '@userActions/Session'; -import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -74,27 +74,27 @@ function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, che const isTaskCompleted = !isEmptyObject(taskReport) ? taskReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && taskReport.statusNum === CONST.REPORT.STATUS_NUM.APPROVED : action?.childStateNum === CONST.REPORT.STATE_NUM.APPROVED && action?.childStatusNum === CONST.REPORT.STATUS_NUM.APPROVED; - const taskTitle = Str.htmlEncode(TaskUtils.getTaskTitleFromReport(taskReport, action?.childReportName ?? '')); - const taskAssigneeAccountID = Task.getTaskAssigneeAccountID(taskReport) ?? action?.childManagerAccountID ?? CONST.DEFAULT_NUMBER_ID; + const taskTitle = Str.htmlEncode(getTaskTitleFromReport(taskReport, action?.childReportName ?? '')); + const taskAssigneeAccountID = getTaskAssigneeAccountID(taskReport) ?? action?.childManagerAccountID ?? CONST.DEFAULT_NUMBER_ID; const taskOwnerAccountID = taskReport?.ownerAccountID ?? action?.actorAccountID ?? CONST.DEFAULT_NUMBER_ID; const hasAssignee = taskAssigneeAccountID > 0; const personalDetails = usePersonalDetails(); const avatar = personalDetails?.[taskAssigneeAccountID]?.avatar ?? Expensicons.FallbackAvatar; const avatarSize = CONST.AVATAR_SIZE.SMALL; - const isDeletedParentAction = ReportUtils.isCanceledTaskReport(taskReport, action); + const isDeletedParentAction = isCanceledTaskReport(taskReport, action); const iconWrapperStyle = StyleUtils.getTaskPreviewIconWrapper(hasAssignee ? avatarSize : undefined); const titleStyle = StyleUtils.getTaskPreviewTitleStyle(iconWrapperStyle.height, isTaskCompleted); - const shouldShowGreenDotIndicator = ReportUtils.isOpenTaskReport(taskReport, action) && ReportUtils.isReportManager(taskReport); + const shouldShowGreenDotIndicator = isOpenTaskReport(taskReport, action) && isReportManager(taskReport); if (isDeletedParentAction) { - return ${translate('parentReportAction.deletedTask')}`} />; + return ${translate('parentReportAction.deletedTask')}`} />; } return ( Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(taskReportID))} - onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + onPressIn={() => canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} onLongPress={(event) => showContextMenuForReport(event, contextMenuAnchor, chatReportID, action, checkIfContextMenuActive)} shouldUseHapticsOnLongPress @@ -107,12 +107,12 @@ function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, che { + disabled={!canActionTask(taskReport, currentUserPersonalDetails.accountID, taskOwnerAccountID, taskAssigneeAccountID)} + onPress={checkIfActionIsAllowed(() => { if (isTaskCompleted) { - Task.reopenTask(taskReport, taskReportID); + reopenTask(taskReport, taskReportID); } else { - Task.completeTask(taskReport, taskReportID); + completeTask(taskReport, taskReportID); } })} accessibilityLabel={translate('task.task')} diff --git a/src/languages/en.ts b/src/languages/en.ts index 7c3bd33c8b2a..3b55d6fbc75c 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4974,12 +4974,12 @@ const translations = { viewAttachment: 'View attachment', }, parentReportAction: { - deletedReport: '[Deleted report]', - deletedMessage: '[Deleted message]', - deletedExpense: '[Deleted expense]', - reversedTransaction: '[Reversed transaction]', - deletedTask: '[Deleted task]', - hiddenMessage: '[Hidden message]', + deletedReport: 'Deleted report', + deletedMessage: 'Deleted message', + deletedExpense: 'Deleted expense', + reversedTransaction: 'Reversed transaction', + deletedTask: 'Deleted task', + hiddenMessage: 'Hidden message', }, threads: { thread: 'Thread', diff --git a/src/languages/es.ts b/src/languages/es.ts index 140891c7a4fa..dff3dcd575c0 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5486,12 +5486,12 @@ const translations = { viewAttachment: 'Ver archivo adjunto', }, parentReportAction: { - deletedReport: '[Informe eliminado]', - deletedMessage: '[Mensaje eliminado]', - deletedExpense: '[Gasto eliminado]', - reversedTransaction: '[Transacción anulada]', - deletedTask: '[Tarea eliminada]', - hiddenMessage: '[Mensaje oculto]', + deletedReport: 'Informe eliminado', + deletedMessage: 'Mensaje eliminado', + deletedExpense: 'Gasto eliminado', + reversedTransaction: 'Transacción anulada', + deletedTask: 'Tarea eliminada', + hiddenMessage: 'Mensaje oculto', }, threads: { thread: 'Hilo', diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index ee09576d1a4d..c74531acf317 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -733,7 +733,7 @@ function PureReportActionItem({ ); } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW) { children = isClosedExpenseReportWithNoExpenses ? ( - ${translate('parentReportAction.deletedReport')}`} /> + ${translate('parentReportAction.deletedReport')}`} /> ) : ( @@ -76,13 +75,13 @@ function ReportActionItemContentCreated({contextValue, parentReportAction, trans const contextMenuValue = useMemo(() => ({...contextValue, isDisabled: true}), [contextValue]); - if (ReportActionsUtils.isTransactionThread(parentReportAction)) { - const isReversedTransaction = ReportActionsUtils.isReversedTransaction(parentReportAction); + if (isTransactionThread(parentReportAction)) { + const isTransactionReversed = isReversedTransaction(parentReportAction); - if (ReportActionsUtils.isMessageDeleted(parentReportAction) || isReversedTransaction) { + if (isMessageDeleted(parentReportAction) || isTransactionReversed) { let message: TranslationPaths; - if (isReversedTransaction) { + if (isTransactionReversed) { message = 'parentReportAction.reversedTransaction'; } else { message = 'parentReportAction.deletedExpense'; @@ -97,7 +96,7 @@ function ReportActionItemContentCreated({contextValue, parentReportAction, trans showHeader report={report} > - ${translate(message)}`} /> + ${translate(message)}`} /> @@ -120,8 +119,8 @@ function ReportActionItemContentCreated({contextValue, parentReportAction, trans ); } - if (ReportUtils.isTaskReport(report)) { - if (ReportUtils.isCanceledTaskReport(report, parentReportAction)) { + if (isTaskReport(report)) { + if (isCanceledTaskReport(report, parentReportAction)) { return ( @@ -131,7 +130,7 @@ function ReportActionItemContentCreated({contextValue, parentReportAction, trans showHeader={draftMessage === undefined} report={report} > - ${translate('parentReportAction.deletedTask')}`} /> + ${translate('parentReportAction.deletedTask')}`} /> @@ -150,7 +149,7 @@ function ReportActionItemContentCreated({contextValue, parentReportAction, trans ); } - if (ReportUtils.isExpenseReport(report) || ReportUtils.isIOUReport(report) || ReportUtils.isInvoiceReport(report)) { + if (isExpenseReport(report) || isIOUReport(report) || isInvoiceReport(report)) { return ( {!isEmptyObject(transactionThreadReport?.reportID) ? ( diff --git a/src/pages/home/report/ReportActionItemFragment.tsx b/src/pages/home/report/ReportActionItemFragment.tsx index 05cb657b1e54..50721a5ffcad 100644 --- a/src/pages/home/report/ReportActionItemFragment.tsx +++ b/src/pages/home/report/ReportActionItemFragment.tsx @@ -6,7 +6,7 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import convertToLTR from '@libs/convertToLTR'; -import * as ReportUtils from '@libs/ReportUtils'; +import isReportMessageAttachment from '@libs/isReportMessageAttachment'; import CONST from '@src/CONST'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type {DecisionName, OriginalMessageSource} from '@src/types/onyx/OriginalMessage'; @@ -106,14 +106,14 @@ function ReportActionItemFragment({ // immediately display "[Deleted message]" while the delete action is pending. if ((!isOffline && isThreadParentMessage && isPendingDelete) || fragment?.isDeletedParentAction) { - return ${translate('parentReportAction.deletedMessage')}`} />; + return ${translate('parentReportAction.deletedMessage')}`} />; } if (isThreadParentMessage && moderationDecision === CONST.MODERATION.MODERATOR_DECISION_PENDING_REMOVE) { - return ${translate('parentReportAction.hiddenMessage')}`} />; + return ${translate('parentReportAction.hiddenMessage')}`} />; } - if (ReportUtils.isReportMessageAttachment(fragment)) { + if (isReportMessageAttachment(fragment)) { return (