diff --git a/src/components/MultipleAvatars.js b/src/components/MultipleAvatars.js
index 9b4d10c5c8e8..daeb7a1c780d 100644
--- a/src/components/MultipleAvatars.js
+++ b/src/components/MultipleAvatars.js
@@ -182,7 +182,12 @@ const MultipleAvatars = (props) => {
absolute
>
- {`+${props.icons.length - 1}`}
+
+ {`+${props.icons.length - 1}`}
+
)}
diff --git a/src/languages/en.js b/src/languages/en.js
index 9cb699f5f838..22ac07e014f6 100755
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -1310,4 +1310,9 @@ export default {
parentReportAction: {
deletedMessage: '[Deleted message]',
},
+ threads: {
+ lastReply: 'Last Reply',
+ replies: 'Replies',
+ reply: 'Reply',
+ },
};
diff --git a/src/languages/es.js b/src/languages/es.js
index 7eab68f8cf06..003192a3c8cc 100644
--- a/src/languages/es.js
+++ b/src/languages/es.js
@@ -1775,4 +1775,9 @@ export default {
parentReportAction: {
deletedMessage: '[Mensaje eliminado]',
},
+ threads: {
+ lastReply: 'Ăšltima respuesta',
+ replies: 'Respuestas',
+ reply: 'Respuesta',
+ },
};
diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js
index bf536690f364..7b0fd560ffe1 100644
--- a/src/libs/ReportUtils.js
+++ b/src/libs/ReportUtils.js
@@ -447,6 +447,17 @@ function isThreadParent(reportAction) {
return reportAction && reportAction.childReportID && reportAction.childReportID !== 0;
}
+/**
+ * Returns true if reportAction is the first chat preview of a Thread
+ *
+ * @param {Object} reportAction
+ * @param {String} reportID
+ * @returns {Boolean}
+ */
+function isThreadFirstChat(reportAction, reportID) {
+ return !_.isUndefined(reportAction.childReportID) && reportAction.childReportID.toString() === reportID;
+}
+
/**
* Get either the policyName or domainName the chat is tied to
* @param {Object} report
@@ -721,6 +732,41 @@ function getSmallSizeAvatar(avatarURL, login) {
return `${source.substring(0, lastPeriodIndex)}_128${source.substring(lastPeriodIndex)}`;
}
+/**
+ * Returns the appropriate icons for the given chat report using the stored personalDetails.
+ * The Avatar sources can be URLs or Icon components according to the chat type.
+ *
+ * @param {Array} participants
+ * @param {Object} personalDetails
+ * @returns {Array<*>}
+ */
+function getIconsForParticipants(participants, personalDetails) {
+ const participantDetails = [];
+ const participantsList = participants || [];
+
+ for (let i = 0; i < participantsList.length; i++) {
+ const login = participantsList[i];
+ const avatarSource = getAvatar(lodashGet(personalDetails, [login, 'avatar'], ''), login);
+ participantDetails.push([login, lodashGet(personalDetails, [login, 'firstName'], ''), avatarSource]);
+ }
+
+ // Sort all logins by first name (which is the second element in the array)
+ const sortedParticipantDetails = participantDetails.sort((a, b) => a[1] - b[1]);
+
+ // Now that things are sorted, gather only the avatars (third element in the array) and return those
+ const avatars = [];
+ for (let i = 0; i < sortedParticipantDetails.length; i++) {
+ const userIcon = {
+ source: sortedParticipantDetails[i][2],
+ type: CONST.ICON_TYPE_AVATAR,
+ name: sortedParticipantDetails[i][0],
+ };
+ avatars.push(userIcon);
+ }
+
+ return avatars;
+}
+
/**
* Returns the appropriate icons for the given chat report using the stored personalDetails.
* The Avatar sources can be URLs or Icon components according to the chat type.
@@ -825,30 +871,7 @@ function getIcons(report, personalDetails, defaultIcon = null) {
];
}
- const participantDetails = [];
- const participants = report.participants || [];
-
- for (let i = 0; i < participants.length; i++) {
- const login = participants[i];
- const avatarSource = getAvatar(lodashGet(personalDetails, [login, 'avatar'], ''), login);
- participantDetails.push([login, lodashGet(personalDetails, [login, 'firstName'], ''), avatarSource]);
- }
-
- // Sort all logins by first name (which is the second element in the array)
- const sortedParticipantDetails = participantDetails.sort((a, b) => a[1] - b[1]);
-
- // Now that things are sorted, gather only the avatars (third element in the array) and return those
- const avatars = [];
- for (let i = 0; i < sortedParticipantDetails.length; i++) {
- const userIcon = {
- source: sortedParticipantDetails[i][2],
- type: CONST.ICON_TYPE_AVATAR,
- name: sortedParticipantDetails[i][0],
- };
- avatars.push(userIcon);
- }
-
- return avatars;
+ return getIconsForParticipants(report.participants, personalDetails);
}
/**
@@ -2020,6 +2043,7 @@ export {
chatIncludesConcierge,
isPolicyExpenseChat,
getDefaultAvatar,
+ getIconsForParticipants,
getIcons,
getRoomWelcomeMessage,
getDisplayNamesWithTooltips,
@@ -2069,6 +2093,7 @@ export {
getWorkspaceAvatar,
isThread,
isThreadParent,
+ isThreadFirstChat,
shouldReportShowSubscript,
isSettled,
};
diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js
index 9f95c0a8bf97..2b56094afc2b 100644
--- a/src/pages/home/report/ContextMenu/ContextMenuActions.js
+++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js
@@ -114,7 +114,7 @@ export default [
Permissions.canUseThreads(betas) &&
type === CONTEXT_MENU_TYPES.REPORT_ACTION &&
reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT &&
- (_.isUndefined(reportAction.childReportID) || reportAction.childReportID.toString() !== reportID),
+ !ReportUtils.isThreadFirstChat(reportAction, reportID),
onPress: (closePopover, {reportAction, reportID}) => {
Report.navigateToAndOpenChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID);
if (closePopover) {
diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js
index 0090396e61cf..38b5bda6f713 100644
--- a/src/pages/home/report/ReportActionItem.js
+++ b/src/pages/home/report/ReportActionItem.js
@@ -17,6 +17,7 @@ import ReportActionItemMessage from './ReportActionItemMessage';
import UnreadActionIndicator from '../../../components/UnreadActionIndicator';
import ReportActionItemMessageEdit from './ReportActionItemMessageEdit';
import ReportActionItemCreated from './ReportActionItemCreated';
+import ReportActionItemThread from './ReportActionItemThread';
import compose from '../../../libs/compose';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions';
import ControlSelection from '../../../libs/ControlSelection';
@@ -48,6 +49,7 @@ import personalDetailsPropType from '../../personalDetailsPropType';
import ReportActionItemDraft from './ReportActionItemDraft';
import TaskPreview from '../../../components/ReportActionItem/TaskPreview';
import * as ReportActionUtils from '../../../libs/ReportActionsUtils';
+import Permissions from '../../../libs/Permissions';
const propTypes = {
/** Report for this action */
@@ -83,6 +85,9 @@ const propTypes = {
/** All of the personalDetails */
personalDetails: PropTypes.objectOf(personalDetailsPropType),
+ /** List of betas available to current user */
+ betas: PropTypes.arrayOf(PropTypes.string),
+
...windowDimensionsPropTypes,
};
@@ -92,6 +97,7 @@ const defaultProps = {
preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE,
personalDetails: {},
shouldShowSubscriptAvatar: false,
+ betas: [],
};
class ReportActionItem extends Component {
@@ -243,6 +249,10 @@ class ReportActionItem extends Component {
const reactions = _.get(this.props, ['action', 'message', 0, 'reactions'], []);
const hasReactions = reactions.length > 0;
+ const shouldDisplayThreadReplies =
+ this.props.action.childCommenterCount && Permissions.canUseThreads(this.props.betas) && !ReportUtils.isThreadFirstChat(this.props.action, this.props.report.reportID);
+ const oldestFourEmails = lodashGet(this.props.action, 'childOldestFourEmails', '').split(',');
+
return (
<>
{children}
@@ -254,6 +264,14 @@ class ReportActionItem extends Component {
/>
)}
+ {shouldDisplayThreadReplies && (
+
+ )}
>
);
}
@@ -371,6 +389,7 @@ class ReportActionItem extends Component {
isVisible={hovered && !this.props.draftMessage && !hasErrors}
draftMessage={this.props.draftMessage}
isChronosReport={ReportUtils.chatIncludesChronos(this.props.report)}
+ childReportActionID={this.props.action.childReportActionID}
/>
)}
@@ -402,5 +421,8 @@ export default compose(
preferredSkinTone: {
key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE,
},
+ betas: {
+ key: ONYXKEYS.BETAS,
+ },
}),
)(ReportActionItem);
diff --git a/src/pages/home/report/ReportActionItemThread.js b/src/pages/home/report/ReportActionItemThread.js
new file mode 100644
index 000000000000..9292c4b01c37
--- /dev/null
+++ b/src/pages/home/report/ReportActionItemThread.js
@@ -0,0 +1,66 @@
+import React from 'react';
+import {View, Pressable, Text} from 'react-native';
+import PropTypes from 'prop-types';
+import _ from 'underscore';
+import styles from '../../../styles/styles';
+import * as Report from '../../../libs/actions/Report';
+import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
+import CONST from '../../../CONST';
+import avatarPropTypes from '../../../components/avatarPropTypes';
+import MultipleAvatars from '../../../components/MultipleAvatars';
+import Navigation from '../../../libs/Navigation/Navigation';
+import ROUTES from '../../../ROUTES';
+
+const propTypes = {
+ /** List of participant icons for the thread */
+ icons: PropTypes.arrayOf(avatarPropTypes).isRequired,
+
+ /** Number of comments under the thread */
+ numberOfReplies: PropTypes.number.isRequired,
+
+ /** Time of the most recent reply */
+ mostRecentReply: PropTypes.string.isRequired,
+
+ /** ID of child thread report */
+ childReportID: PropTypes.string.isRequired,
+
+ /** localization props */
+ ...withLocalizePropTypes,
+};
+
+const ReportActionItemThread = (props) => (
+
+ {
+ Report.openReport(props.childReportID);
+ Navigation.navigate(ROUTES.getReportRoute(props.childReportID));
+ }}
+ >
+
+ icon.name)}
+ />
+
+
+ {`${props.numberOfReplies} ${props.numberOfReplies === 1 ? props.translate('threads.reply') : props.translate('threads.replies')}`}
+
+ {`${props.translate('threads.lastReply')} ${props.datetimeToCalendarTime(props.mostRecentReply)}`}
+
+
+
+
+);
+
+ReportActionItemThread.propTypes = propTypes;
+ReportActionItemThread.displayName = 'ReportActionItemThread';
+
+export default withLocalize(ReportActionItemThread);
diff --git a/src/styles/styles.js b/src/styles/styles.js
index b09ea373140b..bdb1ebd7469d 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -1018,6 +1018,10 @@ const styles = {
lineHeight: 16,
},
+ lh140Percent: {
+ lineHeight: '140%',
+ },
+
formHelp: {
color: themeColors.textSupporting,
fontSize: variables.fontSizeLabel,