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);
});
});
});