diff --git a/assets/images/thread.svg b/assets/images/thread.svg new file mode 100644 index 000000000000..3b8f334fafdd --- /dev/null +++ b/assets/images/thread.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index 396c10151fbf..f6afb4dae2d6 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -141,6 +141,7 @@ function AvatarWithDisplayName({ )} diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 5191d2012b05..1fcf0d07276c 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -141,6 +141,7 @@ import Sync from '@assets/images/sync.svg'; import Tag from '@assets/images/tag.svg'; import Task from '@assets/images/task.svg'; import Tax from '@assets/images/tax.svg'; +import Thread from '@assets/images/thread.svg'; import ThreeDots from '@assets/images/three-dots.svg'; import ThumbsUp from '@assets/images/thumbs-up.svg'; import Transfer from '@assets/images/transfer.svg'; @@ -230,6 +231,7 @@ export { Folder, Tag, Tax, + Thread, Gallery, Gear, Globe, diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index 3109453ca6b0..d36a2e93f5b3 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -15,11 +15,14 @@ type ParentNavigationSubtitleProps = { /** parent Report ID */ parentReportID?: string; + /** parent Report Action ID */ + parentReportActionID?: string; + /** PressableWithoutFeedack additional styles */ pressableStyles?: StyleProp; }; -function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportID = '', pressableStyles}: ParentNavigationSubtitleProps) { +function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportActionID, parentReportID = '', pressableStyles}: ParentNavigationSubtitleProps) { const styles = useThemeStyles(); const {workspaceName, reportName} = parentNavigationSubtitleData; @@ -28,7 +31,7 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportID return ( { - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(parentReportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(parentReportID, parentReportActionID)); }} accessibilityLabel={translate('threads.parentNavigationSummary', {reportName, workspaceName})} role={CONST.ROLE.LINK} diff --git a/src/languages/en.ts b/src/languages/en.ts index 4badcddbc03d..d793122578d1 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2408,6 +2408,7 @@ export default { hiddenMessage: '[Hidden message]', }, threads: { + thread: 'Thread', replies: 'Replies', reply: 'Reply', from: 'From', diff --git a/src/languages/es.ts b/src/languages/es.ts index 8167633c2d64..7fa1042513a5 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2900,6 +2900,7 @@ export default { hiddenMessage: '[Mensaje oculto]', }, threads: { + thread: 'Hilo', replies: 'Respuestas', reply: 'Respuesta', from: 'De', diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ce4264b32141..033a105a9da7 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -419,7 +419,6 @@ type Ancestor = { report: Report; reportAction: ReportAction; shouldDisplayNewMarker: boolean; - shouldHideThreadDividerLine: boolean; }; type AncestorIDs = { @@ -5342,7 +5341,7 @@ function shouldDisableThread(reportAction: OnyxEntry, reportID: st ); } -function getAllAncestorReportActions(report: Report | null | undefined, shouldHideThreadDividerLine: boolean): Ancestor[] { +function getAllAncestorReportActions(report: Report | null | undefined): Ancestor[] { if (!report) { return []; } @@ -5352,7 +5351,6 @@ function getAllAncestorReportActions(report: Report | null | undefined, shouldHi // Store the child of parent report let currentReport = report; - let currentUnread = shouldHideThreadDividerLine; while (parentReportID) { const parentReport = getReport(parentReportID); @@ -5367,14 +5365,11 @@ function getAllAncestorReportActions(report: Report | null | undefined, shouldHi report: currentReport, reportAction: parentReportAction, shouldDisplayNewMarker: isParentReportActionUnread, - // We should hide the thread divider line if the previous ancestor action is unread - shouldHideThreadDividerLine: currentUnread, }); parentReportID = parentReport?.parentReportID; parentReportActionID = parentReport?.parentReportActionID; if (!isEmptyObject(parentReport)) { currentReport = parentReport; - currentUnread = isParentReportActionUnread; } } diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index f06b40af8851..80563fcf7b1b 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -252,6 +252,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD )} diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx index 9d620472bf3a..acf57dd68fe7 100644 --- a/src/pages/home/HeaderView.tsx +++ b/src/pages/home/HeaderView.tsx @@ -272,6 +272,7 @@ function HeaderView({report, personalDetails, parentReport, parentReportAction, )} diff --git a/src/pages/home/report/RepliesDivider.tsx b/src/pages/home/report/RepliesDivider.tsx new file mode 100644 index 000000000000..deac38596c99 --- /dev/null +++ b/src/pages/home/report/RepliesDivider.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import {View} from 'react-native'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; + +type RepliesDividerProps = { + /** Whether we should hide thread divider line */ + shouldHideThreadDividerLine: boolean; +}; + +function RepliesDivider({shouldHideThreadDividerLine}: RepliesDividerProps) { + const styles = useThemeStyles(); + const theme = useTheme(); + const {translate} = useLocalize(); + + return ( + + + {translate('threads.replies')} + {!shouldHideThreadDividerLine && } + + ); +} + +RepliesDivider.displayName = 'RepliesDivider'; +export default RepliesDivider; diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index 21828eb3d116..f441f8e0ea3f 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -827,7 +827,7 @@ function ReportActionItem({ checkIfContextMenuActive={toggleContextMenuFromActiveReportAction} setIsEmojiPickerActive={setIsEmojiPickerActive} /> - + ReportActions.clearAllRelatedReportActionErrors(report.reportID, action)} // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing diff --git a/src/pages/home/report/ReportActionItemParentAction.tsx b/src/pages/home/report/ReportActionItemParentAction.tsx index 7185ab728ccd..7dc5ace631fa 100644 --- a/src/pages/home/report/ReportActionItemParentAction.tsx +++ b/src/pages/home/report/ReportActionItemParentAction.tsx @@ -13,7 +13,9 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; +import RepliesDivider from './RepliesDivider'; import ReportActionItem from './ReportActionItem'; +import ThreadDivider from './ThreadDivider'; type ReportActionItemParentActionProps = { /** Flag to show, hide the thread divider line */ @@ -31,9 +33,12 @@ type ReportActionItemParentActionProps = { /** Report actions belonging to the report's parent */ parentReportAction: OnyxEntry; + + /** Whether we should display "Replies" divider */ + shouldDisplayReplyDivider: boolean; }; -function ReportActionItemParentAction({report, parentReportAction, index = 0, shouldHideThreadDividerLine = false}: ReportActionItemParentActionProps) { +function ReportActionItemParentAction({report, parentReportAction, index = 0, shouldHideThreadDividerLine = false, shouldDisplayReplyDivider}: ReportActionItemParentActionProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -48,7 +53,7 @@ function ReportActionItemParentAction({report, parentReportAction, index = 0, sh onyxSubscribe({ key: `${ONYXKEYS.COLLECTION.REPORT}${ancestorReportID}`, callback: () => { - setAllAncestors(ReportUtils.getAllAncestorReportActions(report, shouldHideThreadDividerLine)); + setAllAncestors(ReportUtils.getAllAncestorReportActions(report)); }, }), ); @@ -56,7 +61,7 @@ function ReportActionItemParentAction({report, parentReportAction, index = 0, sh onyxSubscribe({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestorReportID}`, callback: () => { - setAllAncestors(ReportUtils.getAllAncestorReportActions(report, shouldHideThreadDividerLine)); + setAllAncestors(ReportUtils.getAllAncestorReportActions(report)); }, }), ); @@ -82,8 +87,9 @@ function ReportActionItemParentAction({report, parentReportAction, index = 0, sh errorRowStyles={[styles.ml10, styles.mr2]} onClose={() => Report.navigateToConciergeChatAndDeleteReport(ancestor.report.reportID)} > + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.reportID))} + onPress={() => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.parentReportID ?? '', ancestor.reportAction.reportActionID))} parentReportAction={parentReportAction} report={ancestor.report} action={ancestor.reportAction} @@ -92,9 +98,9 @@ function ReportActionItemParentAction({report, parentReportAction, index = 0, sh shouldDisplayNewMarker={ancestor.shouldDisplayNewMarker} index={index} /> - {!ancestor.shouldHideThreadDividerLine && } ))} + {shouldDisplayReplyDivider && } ); } diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 7d90d98fecf7..3b001d859ed8 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -522,9 +522,19 @@ function ReportActionsList({ mostRecentIOUReportActionID={mostRecentIOUReportActionID} shouldHideThreadDividerLine={shouldHideThreadDividerLine} shouldDisplayNewMarker={shouldDisplayNewMarker(reportAction, index)} + shouldDisplayReplyDivider={sortedReportActions.length > 1} /> ), - [report, linkedReportActionID, sortedVisibleReportActions, mostRecentIOUReportActionID, shouldHideThreadDividerLine, shouldDisplayNewMarker, parentReportAction], + [ + report, + linkedReportActionID, + sortedVisibleReportActions, + sortedReportActions.length, + mostRecentIOUReportActionID, + shouldHideThreadDividerLine, + shouldDisplayNewMarker, + parentReportAction, + ], ); // Native mobile does not render updates flatlist the changes even though component did update called. diff --git a/src/pages/home/report/ReportActionsListItemRenderer.tsx b/src/pages/home/report/ReportActionsListItemRenderer.tsx index fb51753e3eb7..4ea395c61100 100644 --- a/src/pages/home/report/ReportActionsListItemRenderer.tsx +++ b/src/pages/home/report/ReportActionsListItemRenderer.tsx @@ -34,6 +34,9 @@ type ReportActionsListItemRendererProps = { /** Linked report action ID */ linkedReportActionID?: string; + + /** Whether we should display "Replies" divider */ + shouldDisplayReplyDivider: boolean; }; function ReportActionsListItemRenderer({ @@ -46,6 +49,7 @@ function ReportActionsListItemRenderer({ shouldHideThreadDividerLine, shouldDisplayNewMarker, linkedReportActionID = '', + shouldDisplayReplyDivider, }: ReportActionsListItemRendererProps) { const shouldDisplayParentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED && ReportUtils.isChatThread(report) && !ReportActionsUtils.isTransactionThread(parentReportAction); @@ -119,6 +123,7 @@ function ReportActionsListItemRenderer({ return shouldDisplayParentAction ? ( + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(ancestor?.report?.parentReportID ?? '', ancestor.reportAction.reportActionID))} + accessibilityLabel={translate('threads.thread')} + role={CONST.ROLE.BUTTON} + style={[styles.flexRow, styles.alignItemsCenter, styles.gap1]} + > + + {translate('threads.thread')} + + {!ancestor.shouldDisplayNewMarker && } + + ); +} + +ThreadDivider.displayName = 'ThreadDivider'; +export default ThreadDivider; diff --git a/src/styles/index.ts b/src/styles/index.ts index a5d1fff864e9..e49abf9f5f9f 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -1898,7 +1898,6 @@ const styles = (theme: ThemeColors) => fontFamily: FontUtils.fontFamily.platform.EXP_NEUE, lineHeight: variables.lineHeightXLarge, maxWidth: '100%', - ...cursor.cursorAuto, ...whiteSpace.preWrap, ...wordBreak.breakWord, }, @@ -2743,7 +2742,8 @@ const styles = (theme: ThemeColors) => height: 1, backgroundColor: theme.border, flexGrow: 1, - marginHorizontal: 20, + marginLeft: 8, + marginRight: 20, }, unreadIndicatorText: { @@ -2754,6 +2754,12 @@ const styles = (theme: ThemeColors) => textTransform: 'capitalize', }, + threadDividerText: { + fontFamily: FontUtils.fontFamily.platform.EXP_NEUE, + fontSize: variables.fontSizeSmall, + textTransform: 'capitalize', + }, + flipUpsideDown: { transform: `rotate(180deg)`, }, diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 152e023b7d94..b547f28137b3 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1454,7 +1454,7 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ /** * Generate the styles for the ReportActionItem wrapper view. */ - getReportActionItemStyle: (isHovered = false): ViewStyle => + getReportActionItemStyle: (isHovered = false, isClickable = false): ViewStyle => // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) // eslint-disable-next-line @typescript-eslint/no-unsafe-return ({ @@ -1465,7 +1465,7 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ : // Warning: Setting this to a non-transparent color will cause unread indicator to break on Android theme.transparent, opacity: 1, - ...styles.cursorInitial, + ...(isClickable ? styles.cursorPointer : styles.cursorInitial), }), /** diff --git a/tests/unit/ReportUtilsTest.js b/tests/unit/ReportUtilsTest.js index 7b563d46b7eb..ffd5c9147dc0 100644 --- a/tests/unit/ReportUtilsTest.js +++ b/tests/unit/ReportUtilsTest.js @@ -747,34 +747,13 @@ describe('ReportUtils', () => { it('should return correctly all ancestors of a thread report', () => { const resultAncestors = [ - {report: reports[1], reportAction: reportActions[0], shouldDisplayNewMarker: false, shouldHideThreadDividerLine: false}, - {report: reports[2], reportAction: reportActions[1], shouldDisplayNewMarker: false, shouldHideThreadDividerLine: false}, - {report: reports[3], reportAction: reportActions[2], shouldDisplayNewMarker: false, shouldHideThreadDividerLine: false}, - {report: reports[4], reportAction: reportActions[3], shouldDisplayNewMarker: false, shouldHideThreadDividerLine: false}, + {report: reports[1], reportAction: reportActions[0], shouldDisplayNewMarker: false}, + {report: reports[2], reportAction: reportActions[1], shouldDisplayNewMarker: false}, + {report: reports[3], reportAction: reportActions[2], shouldDisplayNewMarker: false}, + {report: reports[4], reportAction: reportActions[3], shouldDisplayNewMarker: false}, ]; - expect(ReportUtils.getAllAncestorReportActions(reports[4], false)).toEqual(resultAncestors); - }); - - it('should hide thread divider line of the nearest ancestor if the first action of thread report is unread', () => { - const allAncestors = ReportUtils.getAllAncestorReportActions(reports[4], true); - expect(allAncestors.reverse()[0].shouldHideThreadDividerLine).toBe(true); - }); - - it('should hide thread divider line of the previous ancestor and display unread marker of the current ancestor if the current ancestor action is unread', () => { - let allAncestors = ReportUtils.getAllAncestorReportActions(reports[4], false); - expect(allAncestors[0].shouldHideThreadDividerLine).toBe(false); - expect(allAncestors[1].shouldDisplayNewMarker).toBe(false); - - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}2`, { - lastReadTime: '2024-02-01 04:42:28.001', - }) - .then(() => waitForBatchedUpdates()) - .then(() => { - allAncestors = ReportUtils.getAllAncestorReportActions(reports[4], false); - expect(allAncestors[0].shouldHideThreadDividerLine).toBe(true); - expect(allAncestors[1].shouldDisplayNewMarker).toBe(true); - }); + expect(ReportUtils.getAllAncestorReportActions(reports[4])).toEqual(resultAncestors); }); }); });