From 982c97b878bce6cba00eaa33f0cb0cd48c83c451 Mon Sep 17 00:00:00 2001 From: Brandon Stites Date: Wed, 26 Apr 2023 18:27:19 -0600 Subject: [PATCH 01/28] Add new button for replying in threads --- src/languages/en.js | 1 + src/languages/es.js | 1 + .../home/report/ContextMenu/ContextMenuActions.js | 14 ++++++++++++++ 3 files changed, 16 insertions(+) diff --git a/src/languages/en.js b/src/languages/en.js index e323fb2f897e..0fc927fb9bd2 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -253,6 +253,7 @@ export default { deleteComment: 'Delete comment', deleteConfirmation: 'Are you sure you want to delete this comment?', onlyVisible: 'Only visible to', + replyInThread: 'Reply in thread', }, emojiReactions: { addReactionTooltip: 'Add reaction', diff --git a/src/languages/es.js b/src/languages/es.js index 0e8c21b50332..700955ee60a8 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -252,6 +252,7 @@ export default { deleteComment: 'Eliminar comentario', deleteConfirmation: '¿Estás seguro de que quieres eliminar este comentario?', onlyVisible: 'Visible sólo para', + replyInThread: 'Responder en el hilo', }, emojiReactions: { addReactionTooltip: 'Añadir una reacción', diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index 385fee477f7b..b6f1151503ce 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -109,6 +109,20 @@ export default [ }, getDescription: () => {}, }, + { + textTranslateKey: 'reportActionContextMenu.replyInThread', + icon: Expensicons.ChatBubble, + successTextTranslateKey: '', + successIcon: null, + shouldShow: type => type === CONTEXT_MENU_TYPES.REPORT_ACTION, + onPress: (closePopover, {reportAction, reportID}) => { + // Report.openChildReport(reportID, reportAction.reportActionID); + if (closePopover) { + hideContextMenu(true, ReportActionComposeFocusManager.focus); + } + }, + getDescription: () => {}, + }, { textTranslateKey: 'reportActionContextMenu.copyURLToClipboard', icon: Expensicons.Copy, From e09ee8b5e7a8c995e905ae82ba9fbdaae24f8a88 Mon Sep 17 00:00:00 2001 From: Brandon Stites Date: Wed, 26 Apr 2023 18:41:19 -0600 Subject: [PATCH 02/28] Add size prop --- src/components/RoomHeaderAvatars.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/RoomHeaderAvatars.js b/src/components/RoomHeaderAvatars.js index eb47af4f6338..c802195ba686 100644 --- a/src/components/RoomHeaderAvatars.js +++ b/src/components/RoomHeaderAvatars.js @@ -12,10 +12,14 @@ import avatarPropTypes from './avatarPropTypes'; const propTypes = { icons: PropTypes.arrayOf(avatarPropTypes), + + /** How large the avatars should be */ + size: PropTypes.string, }; const defaultProps = { icons: [], + size: CONST.AVATAR_SIZE.LARGE_BORDERED, }; const RoomHeaderAvatars = (props) => { @@ -42,7 +46,7 @@ const RoomHeaderAvatars = (props) => { styles.roomHeaderAvatar, // Due to border-box box-sizing, the Avatars have to be larger when bordered to visually match size with non-bordered Avatars - StyleUtils.getAvatarStyle(CONST.AVATAR_SIZE.LARGE_BORDERED), + StyleUtils.getAvatarStyle(props.size), ]; return ( From 1d40aa0d24add0667b6d44b0fed9b271c7c9539f Mon Sep 17 00:00:00 2001 From: Brandon Stites Date: Fri, 28 Apr 2023 11:37:27 -0600 Subject: [PATCH 03/28] Add imageSize method --- src/styles/StyleUtils.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/styles/StyleUtils.js b/src/styles/StyleUtils.js index 1df9d5beccd7..96212b890ba8 100644 --- a/src/styles/StyleUtils.js +++ b/src/styles/StyleUtils.js @@ -54,6 +54,14 @@ const avatarSizes = { [CONST.AVATAR_SIZE.LARGE_BORDERED]: variables.avatarSizeLargeBordered, }; +const avatarImageSizes = { + [CONST.AVATAR_SIZE.SMALL]: styles.avatarSmall, + [CONST.AVATAR_SIZE.DEFAULT]: styles.avatarNormal, + [CONST.AVATAR_SIZE.MEDIUM]: styles.avatarNormal, + [CONST.AVATAR_SIZE.LARGE]: styles.avatarLarge, + [CONST.AVATAR_SIZE.LARGE_BORDERED]: styles.avatarLarge, +}; + /** * Return the style size from an avatar size constant * @@ -110,6 +118,10 @@ function getAvatarBorderStyle(size, type) { }; } +function getAvatarImageStyle(size) { + return avatarImageSizes[size]; +} + /** * Helper method to return old dot default avatar associated with login * @@ -1007,6 +1019,7 @@ export { getAvatarSize, getAvatarStyle, getAvatarBorderStyle, + getAvatarImageStyle, getErrorPageContainerStyle, getSafeAreaPadding, getSafeAreaMargins, From 4b919fda71c0ea8133f8edf3c6bc3990cc7a0797 Mon Sep 17 00:00:00 2001 From: Brandon Stites Date: Fri, 28 Apr 2023 11:37:53 -0600 Subject: [PATCH 04/28] change all sizes based on prop --- src/components/RoomHeaderAvatars.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/RoomHeaderAvatars.js b/src/components/RoomHeaderAvatars.js index c802195ba686..19cc7bdf968d 100644 --- a/src/components/RoomHeaderAvatars.js +++ b/src/components/RoomHeaderAvatars.js @@ -27,13 +27,15 @@ const RoomHeaderAvatars = (props) => { return null; } + const sizeWithoutBorder = props.size === CONST.AVATAR_SIZE.LARGE_BORDERED ? CONST.AVATAR_SIZE.LARGE : props.size; + if (props.icons.length === 1) { return ( @@ -48,6 +50,7 @@ const RoomHeaderAvatars = (props) => { // Due to border-box box-sizing, the Avatars have to be larger when bordered to visually match size with non-bordered Avatars StyleUtils.getAvatarStyle(props.size), ]; + return ( @@ -56,10 +59,10 @@ const RoomHeaderAvatars = (props) => { { styles.roomHeaderAvatarSize, styles.roomHeaderAvatar, ...iconStyle, - StyleUtils.getAvatarBorderRadius(CONST.AVATAR_SIZE.LARGE_BORDERED, icon.type), + StyleUtils.getAvatarBorderRadius(props.size, icon.type), styles.roomHeaderAvatarOverlay, ]} /> From 55aa13bfe8d3bcf60184a94c3aaee12fac7922e4 Mon Sep 17 00:00:00 2001 From: Brandon Stites Date: Fri, 28 Apr 2023 11:38:36 -0600 Subject: [PATCH 05/28] Add threads component --- src/libs/ReportUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 398cd025e598..a8487b80639e 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -641,7 +641,7 @@ function getSmallSizeAvatar(avatarURL, login) { * @param {*} [defaultIcon] * @returns {Array<*>} */ -function getIcons(report, personalDetails, policies, defaultIcon = null) { +function getIcons(report, personalDetails, policies = {}, defaultIcon = null) { const result = { source: '', type: CONST.ICON_TYPE_AVATAR, From 81627040465c9d16767607b659111520e96c66b3 Mon Sep 17 00:00:00 2001 From: Brandon Stites Date: Fri, 28 Apr 2023 11:38:52 -0600 Subject: [PATCH 06/28] Create new component to display threads in reportActions --- src/pages/home/report/ReportActionItem.js | 2 + .../home/report/ReportActionItemThread.js | 49 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 src/pages/home/report/ReportActionItemThread.js diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index a47ff6d4de4e..20c15d2f272a 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'; @@ -240,6 +241,7 @@ class ReportActionItem extends Component { toggleReaction={this.toggleReaction} /> )} + ); } diff --git a/src/pages/home/report/ReportActionItemThread.js b/src/pages/home/report/ReportActionItemThread.js new file mode 100644 index 000000000000..20cc13b8edea --- /dev/null +++ b/src/pages/home/report/ReportActionItemThread.js @@ -0,0 +1,49 @@ +import React from 'react'; +import {View, Pressable, Text} from 'react-native'; +import PropTypes from 'prop-types'; +import _ from 'underscore'; +import lodashGet from 'lodash/get'; +import styles from '../../../styles/styles'; +import ReportActionItemFragment from './ReportActionItemFragment'; +import reportActionPropTypes from './reportActionPropTypes'; +import RoomHeaderAvatars from '../../../components/RoomHeaderAvatars'; +import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; +import CONST from "../../../CONST"; +import avatarPropTypes from "../../../components/avatarPropTypes"; +import themeColors from "../../../styles/themes/default"; + +const propTypes = { + // childReportID: PropTypes.number.isRequired, + + icons: PropTypes.arrayOf(avatarPropTypes).isRequired, + + numberOfReplies: PropTypes.number.isRequired, + + mostRecentReply: PropTypes.string.isRequired, + + /** localization props */ + ...withLocalizePropTypes, +}; + +const ReportActionItemThread = props => ( + + { + // Report.OpenChildReport(props.childReportID) + return ''; + }} + > + + + {`${props.numberOfReplies} Replies`} + {`Last reply at ${props.mostRecentReply}`} + + + + +); + +ReportActionItemThread.propTypes = propTypes; +ReportActionItemThread.displayName = 'ReportActionItemThread'; + +export default withLocalize(ReportActionItemThread); From e7b3b9f8c81e7a8af583673e8539dd262f8ddecd Mon Sep 17 00:00:00 2001 From: Brandon Stites Date: Mon, 1 May 2023 10:00:41 -0600 Subject: [PATCH 07/28] Add method for creating/opening threads --- src/libs/actions/Report.js | 61 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 0890b6e5720e..9b726964b35a 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -389,6 +389,66 @@ function openReport(reportID, participantList = [], newReportObject = {}) { API.write('OpenReport', params, onyxData); } +function openChildReport(childReportID, parentReportAction, parentReport) { + let optimisticReportData = {}; + + let routeReportID = childReportID; + if (childReportID) { + optimisticReportData = { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${childReportID}`, + value: { + isLoadingReportActions: true, + isLoadingMoreReportActions: false, + lastReadTime: DateUtils.getDBTime(), + reportName: lodashGet(allReports, [childReportID, 'reportName'], CONST.REPORT.DEFAULT_REPORT_NAME), + }, + }; + } else if (parentReportAction) { + const newChat = ReportUtils.buildOptimisticChatReport(parentReport.participants, parentReportAction.message[0].html); + routeReportID = newChat.reportID; + optimisticReportData = newChat; + } else { + // throw error + } + + const reportSuccessData = { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${childReportID}`, + value: { + isLoadingReportActions: false, + pendingFields: { + createChat: null, + }, + errorFields: { + createChat: null, + }, + isOptimisticReport: false, + }, + }; + + const reportFailureData = { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${childReportID}`, + value: { + isLoadingReportActions: false, + }, + }; + const onyxData = { + optimisticData: [optimisticReportData], + successData: [reportSuccessData], + failureData: [reportFailureData], + }; + + const params = { + reportID, + parentReportAction: parentReportAction.reportActionID, + }; + + // API.write('OpenChildReport', params, onyxData); + Navigation.navigate(ROUTES.getReportRoute(routeReportID)); +} + /** * This will find an existing chat, or create a new one if none exists, for the given user or set of users. It will then navigate to this chat. * @@ -1485,6 +1545,7 @@ export { readNewestAction, readOldestAction, openReport, + openChildReport, openReportFromDeepLink, navigateToAndOpenReport, openPaymentDetailsPage, From 4db53dafabca87031f227d808a0d5ccf02b0c73a Mon Sep 17 00:00:00 2001 From: Brandon Stites Date: Mon, 1 May 2023 13:51:30 -0600 Subject: [PATCH 08/28] Pass the child reportactionID through the context menu --- .../home/report/ContextMenu/BaseReportActionContextMenu.js | 3 +++ src/pages/home/report/ContextMenu/ContextMenuActions.js | 6 +++--- .../report/ContextMenu/PopoverReportActionContextMenu.js | 5 +++++ .../home/report/ContextMenu/ReportActionContextMenu.js | 3 +++ src/pages/home/report/ReportActionItem.js | 2 ++ src/pages/home/report/ReportActionItemThread.js | 3 +++ 6 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js index 1da859e123c0..63cfcab30229 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js @@ -27,6 +27,8 @@ const propTypes = { /** Whether the provided report is an archived room */ isArchivedRoom: PropTypes.bool, + childReportID: PropTypes.string, + contentRef: PropTypes.oneOfType([PropTypes.node, PropTypes.object, PropTypes.func]), ...genericReportActionContextMenuPropTypes, @@ -74,6 +76,7 @@ class BaseReportActionContextMenu extends React.Component { reportID: this.props.reportID, draftMessage: this.props.draftMessage, selection: this.props.selection, + childReportID: this.props.childReportID, close: () => this.setState({shouldKeepOpen: false}), openContextMenu: () => this.setState({shouldKeepOpen: true}), }; diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index b6f1151503ce..d3e125106373 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -114,9 +114,9 @@ export default [ icon: Expensicons.ChatBubble, successTextTranslateKey: '', successIcon: null, - shouldShow: type => type === CONTEXT_MENU_TYPES.REPORT_ACTION, - onPress: (closePopover, {reportAction, reportID}) => { - // Report.openChildReport(reportID, reportAction.reportActionID); + shouldShow: (type, reportAction) => type === CONTEXT_MENU_TYPES.REPORT_ACTION && reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + onPress: (closePopover, {childReportID, reportAction}) => { + Report.openChildReport(childReportID, reportAction); if (closePopover) { hideContextMenu(true, ReportActionComposeFocusManager.focus); } diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js index 4883d2f37aa7..59c054e06787 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js @@ -39,6 +39,7 @@ class PopoverReportActionContextMenu extends React.Component { }, isArchivedRoom: false, isChronosReport: false, + childReportID: '0', }; this.onPopoverShow = () => {}; this.onPopoverHide = () => {}; @@ -138,6 +139,7 @@ class PopoverReportActionContextMenu extends React.Component { onHide = () => {}, isArchivedRoom, isChronosReport, + childReportID, ) { const nativeEvent = event.nativeEvent || {}; this.contextMenuAnchor = contextMenuAnchor; @@ -168,6 +170,7 @@ class PopoverReportActionContextMenu extends React.Component { reportActionDraftMessage: draftMessage, isArchivedRoom, isChronosReport, + childReportID, }); }); } @@ -273,6 +276,7 @@ class PopoverReportActionContextMenu extends React.Component { shouldSetModalVisibilityForDeleteConfirmation: true, isArchivedRoom: false, isChronosReport: false, + childReportID: '0', }); } @@ -319,6 +323,7 @@ class PopoverReportActionContextMenu extends React.Component { draftMessage={this.state.reportActionDraftMessage} isArchivedRoom={this.state.isArchivedRoom} isChronosReport={this.state.isChronosReport} + childReportID={this.state.childReportID} anchor={this.contextMenuTargetNode} contentRef={this.contentRef} /> diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.js b/src/pages/home/report/ContextMenu/ReportActionContextMenu.js index df544f2e7202..42ef5498e9de 100644 --- a/src/pages/home/report/ContextMenu/ReportActionContextMenu.js +++ b/src/pages/home/report/ContextMenu/ReportActionContextMenu.js @@ -16,6 +16,7 @@ const contextMenuRef = React.createRef(); * @param {Function} [onHide=() => {}] - Run a callback when Menu is hidden * @param {Boolean} isArchivedRoom - Whether the provided report is an archived room * @param {Boolean} isChronosReport - Flag to check if the chat participant is Chronos + * @param {String} childReportID - The child report (thread) of this action */ function showContextMenu( type, @@ -29,6 +30,7 @@ function showContextMenu( onHide = () => {}, isArchivedRoom = false, isChronosReport = false, + childReportID = '', ) { if (!contextMenuRef.current) { return; @@ -45,6 +47,7 @@ function showContextMenu( onHide, isArchivedRoom, isChronosReport, + childReportID, ); } diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 20c15d2f272a..4340bbec6d67 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -160,6 +160,7 @@ class ReportActionItem extends Component { this.checkIfContextMenuActive, ReportUtils.isArchivedRoom(this.props.report), ReportUtils.chatIncludesChronos(this.props.report), + this.props.action.childReportID, ); } @@ -350,6 +351,7 @@ class ReportActionItem extends Component { } draftMessage={this.props.draftMessage} isChronosReport={ReportUtils.chatIncludesChronos(this.props.report)} + childReportActionID={this.props.action.childReportActionID} /> )} diff --git a/src/pages/home/report/ReportActionItemThread.js b/src/pages/home/report/ReportActionItemThread.js index 20cc13b8edea..2630072b3bac 100644 --- a/src/pages/home/report/ReportActionItemThread.js +++ b/src/pages/home/report/ReportActionItemThread.js @@ -6,6 +6,7 @@ import lodashGet from 'lodash/get'; import styles from '../../../styles/styles'; import ReportActionItemFragment from './ReportActionItemFragment'; import reportActionPropTypes from './reportActionPropTypes'; +import * as Report from '../../../libs/actions/Report'; import RoomHeaderAvatars from '../../../components/RoomHeaderAvatars'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; import CONST from "../../../CONST"; @@ -21,6 +22,8 @@ const propTypes = { mostRecentReply: PropTypes.string.isRequired, + childReportID: PropTypes.string.isRequired, + /** localization props */ ...withLocalizePropTypes, }; From 8b31d03f21ed2ff6fc516fd97b2d64f10a3ec7b1 Mon Sep 17 00:00:00 2001 From: Brandon Stites Date: Mon, 1 May 2023 13:54:58 -0600 Subject: [PATCH 09/28] Update API call to properly create optimistic thread report and reportaction --- src/libs/actions/Report.js | 50 ++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 9b726964b35a..7e4dfec1658f 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -389,25 +389,41 @@ function openReport(reportID, participantList = [], newReportObject = {}) { API.write('OpenReport', params, onyxData); } -function openChildReport(childReportID, parentReportAction, parentReport) { - let optimisticReportData = {}; +function openChildReport(childReportID, parentReportAction) { + const successData = []; let routeReportID = childReportID; if (childReportID) { - optimisticReportData = { - onyxMethod: CONST.ONYX.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${childReportID}`, - value: { - isLoadingReportActions: true, - isLoadingMoreReportActions: false, - lastReadTime: DateUtils.getDBTime(), - reportName: lodashGet(allReports, [childReportID, 'reportName'], CONST.REPORT.DEFAULT_REPORT_NAME), + successData.push( + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${childReportID}`, + value: { + isLoadingReportActions: true, + isLoadingMoreReportActions: false, + lastReadTime: DateUtils.getDBTime(), + reportName: lodashGet(allReports, [childReportID, 'reportName'], CONST.REPORT.DEFAULT_REPORT_NAME), + }, }, - }; + ); } else if (parentReportAction) { - const newChat = ReportUtils.buildOptimisticChatReport(parentReport.participants, parentReportAction.message[0].html); + const newChat = ReportUtils.buildOptimisticChatReport([currentUserEmail, parentReportAction.actorEmail], parentReportAction.message[0].html); routeReportID = newChat.reportID; - optimisticReportData = newChat; + successData.push( + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${newChat.reportID}`, + value: newChat, + }, + ); + const parentReportActionID = parentReportAction.reportActionID; + successData.push( + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${newChat.reportID}`, + value: {parentReportActionID: parentReportAction}, + }, + ); } else { // throw error } @@ -435,17 +451,15 @@ function openChildReport(childReportID, parentReportAction, parentReport) { }, }; const onyxData = { - optimisticData: [optimisticReportData], - successData: [reportSuccessData], - failureData: [reportFailureData], + optimisticData: successData, }; const params = { - reportID, + childReportID, parentReportAction: parentReportAction.reportActionID, }; - // API.write('OpenChildReport', params, onyxData); + API.write('OpenChildReport', params, onyxData); Navigation.navigate(ROUTES.getReportRoute(routeReportID)); } From 7761696e62b1be5c84e6020564d5028f89c06a98 Mon Sep 17 00:00:00 2001 From: Brandon Stites Date: Mon, 1 May 2023 16:13:50 -0600 Subject: [PATCH 10/28] go back instead of always home --- src/pages/home/ReportScreen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index df5ba50e622a..865060d23f67 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -275,7 +275,7 @@ class ReportScreen extends React.Component { > Navigation.navigate(ROUTES.HOME)} + onNavigationMenuButtonClicked={Navigation.goBack} personalDetails={this.props.personalDetails} report={this.props.report} policies={this.props.policies} From bae93b4fe90361eabf4a4cebcf49fc5948ef408a Mon Sep 17 00:00:00 2001 From: Brandon Stites Date: Wed, 3 May 2023 10:46:30 -0600 Subject: [PATCH 11/28] Remove changes to navigation --- src/pages/home/ReportScreen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 865060d23f67..df5ba50e622a 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -275,7 +275,7 @@ class ReportScreen extends React.Component { > Navigation.navigate(ROUTES.HOME)} personalDetails={this.props.personalDetails} report={this.props.report} policies={this.props.policies} From 3b73583d0c5970fdd70e16b9f3c68cc2d2dafac8 Mon Sep 17 00:00:00 2001 From: Brandon Stites Date: Wed, 3 May 2023 10:49:06 -0600 Subject: [PATCH 12/28] Remove new openChildReport method --- src/libs/actions/Report.js | 75 +------------------------------------- 1 file changed, 1 insertion(+), 74 deletions(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 7e4dfec1658f..3354bd35a9ad 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -311,6 +311,7 @@ function addComment(reportID, text) { * @param {String} reportID * @param {Array} participantList The list of users that are included in a new chat, not including the user creating it * @param {Object} newReportObject The optimistic report object created when making a new chat, saved as optimistic data + * @param {String} parentReportActionID The report action that a thread was created off of (only passed for threads) */ function openReport(reportID, participantList = [], newReportObject = {}) { const optimisticReportData = { @@ -389,80 +390,6 @@ function openReport(reportID, participantList = [], newReportObject = {}) { API.write('OpenReport', params, onyxData); } -function openChildReport(childReportID, parentReportAction) { - const successData = []; - - let routeReportID = childReportID; - if (childReportID) { - successData.push( - { - onyxMethod: CONST.ONYX.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${childReportID}`, - value: { - isLoadingReportActions: true, - isLoadingMoreReportActions: false, - lastReadTime: DateUtils.getDBTime(), - reportName: lodashGet(allReports, [childReportID, 'reportName'], CONST.REPORT.DEFAULT_REPORT_NAME), - }, - }, - ); - } else if (parentReportAction) { - const newChat = ReportUtils.buildOptimisticChatReport([currentUserEmail, parentReportAction.actorEmail], parentReportAction.message[0].html); - routeReportID = newChat.reportID; - successData.push( - { - onyxMethod: CONST.ONYX.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${newChat.reportID}`, - value: newChat, - }, - ); - const parentReportActionID = parentReportAction.reportActionID; - successData.push( - { - onyxMethod: CONST.ONYX.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${newChat.reportID}`, - value: {parentReportActionID: parentReportAction}, - }, - ); - } else { - // throw error - } - - const reportSuccessData = { - onyxMethod: CONST.ONYX.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${childReportID}`, - value: { - isLoadingReportActions: false, - pendingFields: { - createChat: null, - }, - errorFields: { - createChat: null, - }, - isOptimisticReport: false, - }, - }; - - const reportFailureData = { - onyxMethod: CONST.ONYX.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${childReportID}`, - value: { - isLoadingReportActions: false, - }, - }; - const onyxData = { - optimisticData: successData, - }; - - const params = { - childReportID, - parentReportAction: parentReportAction.reportActionID, - }; - - API.write('OpenChildReport', params, onyxData); - Navigation.navigate(ROUTES.getReportRoute(routeReportID)); -} - /** * This will find an existing chat, or create a new one if none exists, for the given user or set of users. It will then navigate to this chat. * From 10fe957f7d798b4fdfc0c7b91af0cb83c0019c2c Mon Sep 17 00:00:00 2001 From: Brandon Stites Date: Wed, 3 May 2023 20:50:18 -0600 Subject: [PATCH 13/28] Extract more code to other PRs --- src/languages/en.js | 1 - src/languages/es.js | 1 - src/libs/actions/Report.js | 1 - .../ContextMenu/BaseReportActionContextMenu.js | 3 --- .../home/report/ContextMenu/ContextMenuActions.js | 14 -------------- .../ContextMenu/PopoverReportActionContextMenu.js | 5 ----- .../report/ContextMenu/ReportActionContextMenu.js | 3 --- 7 files changed, 28 deletions(-) diff --git a/src/languages/en.js b/src/languages/en.js index 0fc927fb9bd2..e323fb2f897e 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -253,7 +253,6 @@ export default { deleteComment: 'Delete comment', deleteConfirmation: 'Are you sure you want to delete this comment?', onlyVisible: 'Only visible to', - replyInThread: 'Reply in thread', }, emojiReactions: { addReactionTooltip: 'Add reaction', diff --git a/src/languages/es.js b/src/languages/es.js index 700955ee60a8..0e8c21b50332 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -252,7 +252,6 @@ export default { deleteComment: 'Eliminar comentario', deleteConfirmation: '¿Estás seguro de que quieres eliminar este comentario?', onlyVisible: 'Visible sólo para', - replyInThread: 'Responder en el hilo', }, emojiReactions: { addReactionTooltip: 'Añadir una reacción', diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 3354bd35a9ad..20ea4f61ffae 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -1486,7 +1486,6 @@ export { readNewestAction, readOldestAction, openReport, - openChildReport, openReportFromDeepLink, navigateToAndOpenReport, openPaymentDetailsPage, diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js index 63cfcab30229..1da859e123c0 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js @@ -27,8 +27,6 @@ const propTypes = { /** Whether the provided report is an archived room */ isArchivedRoom: PropTypes.bool, - childReportID: PropTypes.string, - contentRef: PropTypes.oneOfType([PropTypes.node, PropTypes.object, PropTypes.func]), ...genericReportActionContextMenuPropTypes, @@ -76,7 +74,6 @@ class BaseReportActionContextMenu extends React.Component { reportID: this.props.reportID, draftMessage: this.props.draftMessage, selection: this.props.selection, - childReportID: this.props.childReportID, close: () => this.setState({shouldKeepOpen: false}), openContextMenu: () => this.setState({shouldKeepOpen: true}), }; diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index d3e125106373..385fee477f7b 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -109,20 +109,6 @@ export default [ }, getDescription: () => {}, }, - { - textTranslateKey: 'reportActionContextMenu.replyInThread', - icon: Expensicons.ChatBubble, - successTextTranslateKey: '', - successIcon: null, - shouldShow: (type, reportAction) => type === CONTEXT_MENU_TYPES.REPORT_ACTION && reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, - onPress: (closePopover, {childReportID, reportAction}) => { - Report.openChildReport(childReportID, reportAction); - if (closePopover) { - hideContextMenu(true, ReportActionComposeFocusManager.focus); - } - }, - getDescription: () => {}, - }, { textTranslateKey: 'reportActionContextMenu.copyURLToClipboard', icon: Expensicons.Copy, diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js index 59c054e06787..4883d2f37aa7 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js @@ -39,7 +39,6 @@ class PopoverReportActionContextMenu extends React.Component { }, isArchivedRoom: false, isChronosReport: false, - childReportID: '0', }; this.onPopoverShow = () => {}; this.onPopoverHide = () => {}; @@ -139,7 +138,6 @@ class PopoverReportActionContextMenu extends React.Component { onHide = () => {}, isArchivedRoom, isChronosReport, - childReportID, ) { const nativeEvent = event.nativeEvent || {}; this.contextMenuAnchor = contextMenuAnchor; @@ -170,7 +168,6 @@ class PopoverReportActionContextMenu extends React.Component { reportActionDraftMessage: draftMessage, isArchivedRoom, isChronosReport, - childReportID, }); }); } @@ -276,7 +273,6 @@ class PopoverReportActionContextMenu extends React.Component { shouldSetModalVisibilityForDeleteConfirmation: true, isArchivedRoom: false, isChronosReport: false, - childReportID: '0', }); } @@ -323,7 +319,6 @@ class PopoverReportActionContextMenu extends React.Component { draftMessage={this.state.reportActionDraftMessage} isArchivedRoom={this.state.isArchivedRoom} isChronosReport={this.state.isChronosReport} - childReportID={this.state.childReportID} anchor={this.contextMenuTargetNode} contentRef={this.contentRef} /> diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.js b/src/pages/home/report/ContextMenu/ReportActionContextMenu.js index 42ef5498e9de..df544f2e7202 100644 --- a/src/pages/home/report/ContextMenu/ReportActionContextMenu.js +++ b/src/pages/home/report/ContextMenu/ReportActionContextMenu.js @@ -16,7 +16,6 @@ const contextMenuRef = React.createRef(); * @param {Function} [onHide=() => {}] - Run a callback when Menu is hidden * @param {Boolean} isArchivedRoom - Whether the provided report is an archived room * @param {Boolean} isChronosReport - Flag to check if the chat participant is Chronos - * @param {String} childReportID - The child report (thread) of this action */ function showContextMenu( type, @@ -30,7 +29,6 @@ function showContextMenu( onHide = () => {}, isArchivedRoom = false, isChronosReport = false, - childReportID = '', ) { if (!contextMenuRef.current) { return; @@ -47,7 +45,6 @@ function showContextMenu( onHide, isArchivedRoom, isChronosReport, - childReportID, ); } From a05a95a85f54ff589a15b2ba0c4a7f216995d1a7 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Thu, 4 May 2023 14:00:17 +0100 Subject: [PATCH 14/28] Clean up thread styles, use Multiple Avatars --- src/pages/home/report/ReportActionItemThread.js | 17 ++++++++++++----- src/styles/styles.js | 4 ++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/pages/home/report/ReportActionItemThread.js b/src/pages/home/report/ReportActionItemThread.js index 2630072b3bac..b4d98c56bb20 100644 --- a/src/pages/home/report/ReportActionItemThread.js +++ b/src/pages/home/report/ReportActionItemThread.js @@ -7,11 +7,11 @@ import styles from '../../../styles/styles'; import ReportActionItemFragment from './ReportActionItemFragment'; import reportActionPropTypes from './reportActionPropTypes'; import * as Report from '../../../libs/actions/Report'; -import RoomHeaderAvatars from '../../../components/RoomHeaderAvatars'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; import CONST from "../../../CONST"; import avatarPropTypes from "../../../components/avatarPropTypes"; import themeColors from "../../../styles/themes/default"; +import MultipleAvatars from '../../../components/MultipleAvatars'; const propTypes = { // childReportID: PropTypes.number.isRequired, @@ -36,10 +36,17 @@ const ReportActionItemThread = props => ( return ''; }} > - - - {`${props.numberOfReplies} Replies`} - {`Last reply at ${props.mostRecentReply}`} + + icon.name)} + /> + + {`${props.numberOfReplies} Replies`} + {`Last reply at ${props.mostRecentReply}`} + diff --git a/src/styles/styles.js b/src/styles/styles.js index fd9551b5b73c..5eb67c4ebd78 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -1004,6 +1004,10 @@ const styles = { lineHeight: 16, }, + lhPercent: { + lineHeight: '140%', + }, + formHelp: { color: themeColors.textSupporting, fontSize: variables.fontSizeLabel, From 0de7775b16360198e12c8780a0225afbc5918490 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Thu, 4 May 2023 14:04:32 +0100 Subject: [PATCH 15/28] Undo changes to RoomHeaderAvatars that are already accomplished by MultipleAvatars --- src/components/RoomHeaderAvatars.js | 19 ++++++------------- src/styles/StyleUtils.js | 13 ------------- 2 files changed, 6 insertions(+), 26 deletions(-) diff --git a/src/components/RoomHeaderAvatars.js b/src/components/RoomHeaderAvatars.js index 19cc7bdf968d..eb47af4f6338 100644 --- a/src/components/RoomHeaderAvatars.js +++ b/src/components/RoomHeaderAvatars.js @@ -12,14 +12,10 @@ import avatarPropTypes from './avatarPropTypes'; const propTypes = { icons: PropTypes.arrayOf(avatarPropTypes), - - /** How large the avatars should be */ - size: PropTypes.string, }; const defaultProps = { icons: [], - size: CONST.AVATAR_SIZE.LARGE_BORDERED, }; const RoomHeaderAvatars = (props) => { @@ -27,15 +23,13 @@ const RoomHeaderAvatars = (props) => { return null; } - const sizeWithoutBorder = props.size === CONST.AVATAR_SIZE.LARGE_BORDERED ? CONST.AVATAR_SIZE.LARGE : props.size; - if (props.icons.length === 1) { return ( @@ -48,9 +42,8 @@ const RoomHeaderAvatars = (props) => { styles.roomHeaderAvatar, // Due to border-box box-sizing, the Avatars have to be larger when bordered to visually match size with non-bordered Avatars - StyleUtils.getAvatarStyle(props.size), + StyleUtils.getAvatarStyle(CONST.AVATAR_SIZE.LARGE_BORDERED), ]; - return ( @@ -59,10 +52,10 @@ const RoomHeaderAvatars = (props) => { { styles.roomHeaderAvatarSize, styles.roomHeaderAvatar, ...iconStyle, - StyleUtils.getAvatarBorderRadius(props.size, icon.type), + StyleUtils.getAvatarBorderRadius(CONST.AVATAR_SIZE.LARGE_BORDERED, icon.type), styles.roomHeaderAvatarOverlay, ]} /> diff --git a/src/styles/StyleUtils.js b/src/styles/StyleUtils.js index afc37ac2ce84..b2cd47beace9 100644 --- a/src/styles/StyleUtils.js +++ b/src/styles/StyleUtils.js @@ -54,14 +54,6 @@ const avatarSizes = { [CONST.AVATAR_SIZE.LARGE_BORDERED]: variables.avatarSizeLargeBordered, }; -const avatarImageSizes = { - [CONST.AVATAR_SIZE.SMALL]: styles.avatarSmall, - [CONST.AVATAR_SIZE.DEFAULT]: styles.avatarNormal, - [CONST.AVATAR_SIZE.MEDIUM]: styles.avatarNormal, - [CONST.AVATAR_SIZE.LARGE]: styles.avatarLarge, - [CONST.AVATAR_SIZE.LARGE_BORDERED]: styles.avatarLarge, -}; - /** * Return the style size from an avatar size constant * @@ -160,10 +152,6 @@ function getAvatarBorderStyle(size, type) { }; } -function getAvatarImageStyle(size) { - return avatarImageSizes[size]; -} - /** * Helper method to return old dot default avatar associated with login * @@ -1107,7 +1095,6 @@ export { getAvatarExtraFontSizeStyle, getAvatarBorderWidth, getAvatarBorderStyle, - getAvatarImageStyle, getErrorPageContainerStyle, getSafeAreaPadding, getSafeAreaMargins, From 9afd2f24fba9501cdf9404bd8aeaf0ec18a0e4d6 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Thu, 4 May 2023 14:12:32 +0100 Subject: [PATCH 16/28] Add translations --- src/languages/en.js | 4 ++++ src/languages/es.js | 4 ++++ src/pages/home/report/ReportActionItemThread.js | 6 +++--- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/languages/en.js b/src/languages/en.js index 0e7a80620c88..8dbd1adbd689 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -1274,4 +1274,8 @@ export default { chatUserDisplayNames: 'Chat user display names', scrollToNewestMessages: 'Scroll to newest messages', }, + threads: { + lastReplyAt: 'Last Reply at', + replies: 'Replies', + }, }; diff --git a/src/languages/es.js b/src/languages/es.js index ef28a83e65b3..2dc915fee8eb 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -1735,4 +1735,8 @@ export default { chatUserDisplayNames: 'Nombres de los usuarios del chat', scrollToNewestMessages: 'Desplázate a los mensajes más recientes', }, + threads: { + lastReplyAt: 'Última respuesta a las', + replies: 'Respuestas', + }, }; diff --git a/src/pages/home/report/ReportActionItemThread.js b/src/pages/home/report/ReportActionItemThread.js index b4d98c56bb20..1537f9d4e551 100644 --- a/src/pages/home/report/ReportActionItemThread.js +++ b/src/pages/home/report/ReportActionItemThread.js @@ -36,7 +36,7 @@ const ReportActionItemThread = props => ( return ''; }} > - + ( avatarTooltips={_.map(props.icons, icon => icon.name)} /> - {`${props.numberOfReplies} Replies`} - {`Last reply at ${props.mostRecentReply}`} + {`${props.numberOfReplies} ${props.translate('threads.replies')}`} + {`${props.translate('threads.lastReplyAt')} ${props.mostRecentReply}`} From c75840a46585dc0f49460d4537a2bc8f8cefb559 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Mon, 8 May 2023 15:28:15 +0100 Subject: [PATCH 17/28] Temporarily reference reportAction --- src/languages/en.js | 3 ++- src/languages/es.js | 3 ++- src/pages/home/report/ReportActionItem.js | 12 +++++++++++- src/pages/home/report/ReportActionItemThread.js | 5 +++-- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/languages/en.js b/src/languages/en.js index 8dbd1adbd689..eb00d4712335 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -1275,7 +1275,8 @@ export default { scrollToNewestMessages: 'Scroll to newest messages', }, threads: { - lastReplyAt: 'Last Reply at', + lastReply: 'Last Reply', replies: 'Replies', + reply: 'Reply', }, }; diff --git a/src/languages/es.js b/src/languages/es.js index 2dc915fee8eb..2ba3702e6fc0 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -1736,7 +1736,8 @@ export default { scrollToNewestMessages: 'Desplázate a los mensajes más recientes', }, threads: { - lastReplyAt: 'Última respuesta a las', + lastReply: 'Última respuesta', replies: 'Respuestas', + reply: 'Respuesta', }, }; diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 387f66637724..ab745ea984ac 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -236,6 +236,9 @@ class ReportActionItem extends Component { const reactions = _.get(this.props, ['action', 'message', 0, 'reactions'], []); const hasReactions = reactions.length > 0; + const shouldDisplayThreadReplies = lodashGet(this.props.action, 'childReportID', 0) !== 0 && lodashGet(this.props.action, 'childCommenterCount', 1) > 0; + // eslint-disable-next-line no-console + console.log('action: ', this.props.action, !_.isEmpty(0)); return ( <> @@ -246,7 +249,14 @@ class ReportActionItem extends Component { toggleReaction={this.toggleReaction} /> )} - + {shouldDisplayThreadReplies && ( + + )} ); } diff --git a/src/pages/home/report/ReportActionItemThread.js b/src/pages/home/report/ReportActionItemThread.js index 1537f9d4e551..48cd76047801 100644 --- a/src/pages/home/report/ReportActionItemThread.js +++ b/src/pages/home/report/ReportActionItemThread.js @@ -12,6 +12,7 @@ import CONST from "../../../CONST"; import avatarPropTypes from "../../../components/avatarPropTypes"; import themeColors from "../../../styles/themes/default"; import MultipleAvatars from '../../../components/MultipleAvatars'; +import ReportActionItemDate from './ReportActionItemDate'; const propTypes = { // childReportID: PropTypes.number.isRequired, @@ -44,8 +45,8 @@ const ReportActionItemThread = props => ( avatarTooltips={_.map(props.icons, icon => icon.name)} /> - {`${props.numberOfReplies} ${props.translate('threads.replies')}`} - {`${props.translate('threads.lastReplyAt')} ${props.mostRecentReply}`} + {`${props.numberOfReplies} ${props.numberOfReplies === 1 ? props.translate('threads.reply') : props.translate('threads.replies')}`} + {`${props.translate('threads.lastReply')} ${props.datetimeToCalendarTime(props.mostRecentReply)}`} From cd52eccbc3f1da713efea3dd733960dc6479037c Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Wed, 10 May 2023 16:48:44 +0100 Subject: [PATCH 18/28] Clean up, icon helper function --- src/libs/ReportUtils.js | 69 +++++++++++-------- src/pages/home/report/ReportActionItem.js | 13 ++-- .../home/report/ReportActionItemThread.js | 30 +++++--- 3 files changed, 68 insertions(+), 44 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 7e31d7fcc909..f2d4bc40f6f9 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -677,6 +677,45 @@ 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 getIconsFromParticipants(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. @@ -758,34 +797,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 getIconsFromParticipants(report.participants, personalDetails); } /** @@ -1841,6 +1853,7 @@ export { chatIncludesConcierge, isPolicyExpenseChat, getDefaultAvatar, + getIconsFromParticipants, getIcons, getRoomWelcomeMessage, getDisplayNamesWithTooltips, diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index ab745ea984ac..703a71f74729 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -236,9 +236,12 @@ class ReportActionItem extends Component { const reactions = _.get(this.props, ['action', 'message', 0, 'reactions'], []); const hasReactions = reactions.length > 0; - const shouldDisplayThreadReplies = lodashGet(this.props.action, 'childReportID', 0) !== 0 && lodashGet(this.props.action, 'childCommenterCount', 1) > 0; - // eslint-disable-next-line no-console - console.log('action: ', this.props.action, !_.isEmpty(0)); + const shouldDisplayThreadReplies = lodashGet(this.props.action, 'childReportID', 0) !== 0 && lodashGet(this.props.action, 'childCommenterCount', 0) > 0; + + let oldestFourEmails = lodashGet(this.props.action, 'childOldestFourEmails', []); + if (!_.isArray(oldestFourEmails)) { + oldestFourEmails = oldestFourEmails.split(','); + } return ( <> @@ -252,9 +255,9 @@ class ReportActionItem extends Component { {shouldDisplayThreadReplies && ( )} diff --git a/src/pages/home/report/ReportActionItemThread.js b/src/pages/home/report/ReportActionItemThread.js index 48cd76047801..788aad09102e 100644 --- a/src/pages/home/report/ReportActionItemThread.js +++ b/src/pages/home/report/ReportActionItemThread.js @@ -2,27 +2,28 @@ import React from 'react'; import {View, Pressable, Text} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; -import lodashGet from 'lodash/get'; import styles from '../../../styles/styles'; -import ReportActionItemFragment from './ReportActionItemFragment'; -import reportActionPropTypes from './reportActionPropTypes'; import * as Report from '../../../libs/actions/Report'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; -import CONST from "../../../CONST"; -import avatarPropTypes from "../../../components/avatarPropTypes"; -import themeColors from "../../../styles/themes/default"; +import CONST from '../../../CONST'; +import avatarPropTypes from '../../../components/avatarPropTypes'; import MultipleAvatars from '../../../components/MultipleAvatars'; -import ReportActionItemDate from './ReportActionItemDate'; +import Navigation from '../../../libs/Navigation/Navigation'; +import ROUTES from '../../../ROUTES'; const propTypes = { - // childReportID: PropTypes.number.isRequired, + /** 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 */ + // eslint-disable-next-line react/no-unused-prop-types childReportID: PropTypes.string.isRequired, /** localization props */ @@ -32,8 +33,11 @@ const propTypes = { const ReportActionItemThread = props => ( { - // Report.OpenChildReport(props.childReportID) + // Report.navigateToAndOpenChildReport(props.childReportID); + // Report.openReport(props.childReportID); + // Navigation.navigate(ROUTES.getReportRoute(props.childReportID)); return ''; }} > @@ -45,8 +49,12 @@ const ReportActionItemThread = props => ( avatarTooltips={_.map(props.icons, icon => icon.name)} /> - {`${props.numberOfReplies} ${props.numberOfReplies === 1 ? props.translate('threads.reply') : props.translate('threads.replies')}`} - {`${props.translate('threads.lastReply')} ${props.datetimeToCalendarTime(props.mostRecentReply)}`} + + {`${props.numberOfReplies} ${props.numberOfReplies === 1 ? props.translate('threads.reply') : props.translate('threads.replies')}`} + + + {`${props.translate('threads.lastReply')} ${props.datetimeToCalendarTime(props.mostRecentReply)}`} + From b54a6b0cd8735f4463e2e8690506dedb6d522575 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Wed, 10 May 2023 16:52:42 +0100 Subject: [PATCH 19/28] Allow to be pressed --- src/pages/home/report/ReportActionItemThread.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pages/home/report/ReportActionItemThread.js b/src/pages/home/report/ReportActionItemThread.js index 788aad09102e..7aa43c214714 100644 --- a/src/pages/home/report/ReportActionItemThread.js +++ b/src/pages/home/report/ReportActionItemThread.js @@ -23,7 +23,6 @@ const propTypes = { mostRecentReply: PropTypes.string.isRequired, /** ID of child thread report */ - // eslint-disable-next-line react/no-unused-prop-types childReportID: PropTypes.string.isRequired, /** localization props */ @@ -35,9 +34,9 @@ const ReportActionItemThread = props => ( { - // Report.navigateToAndOpenChildReport(props.childReportID); - // Report.openReport(props.childReportID); - // Navigation.navigate(ROUTES.getReportRoute(props.childReportID)); + // Replace the following with Report.navigateToAndOpenChildReport(props.childReportID); + Report.openReport(props.childReportID); + Navigation.navigate(ROUTES.getReportRoute(props.childReportID)); return ''; }} > From 00c153220d81ef9b45d5a48298948486e4776866 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Wed, 10 May 2023 21:35:50 +0100 Subject: [PATCH 20/28] fix delimiter --- src/pages/home/report/ReportActionItem.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 8f0b594cee51..bd4859422d2b 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -238,7 +238,7 @@ class ReportActionItem extends Component { let oldestFourEmails = lodashGet(this.props.action, 'childOldestFourEmails', []); if (!_.isArray(oldestFourEmails)) { - oldestFourEmails = oldestFourEmails.split(','); + oldestFourEmails = oldestFourEmails.split(', '); } return ( <> From 20bc93d642eba4d5275cd1fe6694558a6358adab Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Wed, 10 May 2023 21:47:08 +0100 Subject: [PATCH 21/28] undo, my testing data was wrong --- src/pages/home/report/ReportActionItem.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index bd4859422d2b..8f0b594cee51 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -238,7 +238,7 @@ class ReportActionItem extends Component { let oldestFourEmails = lodashGet(this.props.action, 'childOldestFourEmails', []); if (!_.isArray(oldestFourEmails)) { - oldestFourEmails = oldestFourEmails.split(', '); + oldestFourEmails = oldestFourEmails.split(','); } return ( <> From 35492e206bd702ddc61d21b0b5d5e6f51d3f93f7 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Thu, 11 May 2023 10:26:49 +0100 Subject: [PATCH 22/28] Clean up --- src/libs/ReportUtils.js | 6 +----- src/pages/home/report/ReportActionItem.js | 13 +++++-------- src/pages/home/report/ReportActionItemThread.js | 15 ++++----------- src/styles/styles.js | 2 +- 4 files changed, 11 insertions(+), 25 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 98ba424a3237..d664860645ef 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -696,11 +696,7 @@ function getIconsFromParticipants(participants, personalDetails) { 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, - ]); + participantDetails.push([login, lodashGet(personalDetails, [login, 'firstName'], ''), avatarSource]); } // Sort all logins by first name (which is the second element in the array) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 8f0b594cee51..b1f2500f1881 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -234,12 +234,9 @@ class ReportActionItem extends Component { const reactions = _.get(this.props, ['action', 'message', 0, 'reactions'], []); const hasReactions = reactions.length > 0; - const shouldDisplayThreadReplies = lodashGet(this.props.action, 'childReportID', 0) !== 0 && lodashGet(this.props.action, 'childCommenterCount', 0) > 0; + const shouldDisplayThreadReplies = (this.props.action.childCommenterCount || 0) > 0; + const oldestFourEmails = lodashGet(this.props.action, 'childOldestFourEmails', '').split(','); - let oldestFourEmails = lodashGet(this.props.action, 'childOldestFourEmails', []); - if (!_.isArray(oldestFourEmails)) { - oldestFourEmails = oldestFourEmails.split(','); - } return ( <> {children} @@ -253,9 +250,9 @@ class ReportActionItem extends Component { )} {shouldDisplayThreadReplies && ( )} diff --git a/src/pages/home/report/ReportActionItemThread.js b/src/pages/home/report/ReportActionItemThread.js index 7aa43c214714..0d51ebc2f56d 100644 --- a/src/pages/home/report/ReportActionItemThread.js +++ b/src/pages/home/report/ReportActionItemThread.js @@ -12,7 +12,6 @@ import Navigation from '../../../libs/Navigation/Navigation'; import ROUTES from '../../../ROUTES'; const propTypes = { - /** List of participant icons for the thread */ icons: PropTypes.arrayOf(avatarPropTypes).isRequired, @@ -29,15 +28,12 @@ const propTypes = { ...withLocalizePropTypes, }; -const ReportActionItemThread = props => ( +const ReportActionItemThread = (props) => ( { - // Replace the following with Report.navigateToAndOpenChildReport(props.childReportID); Report.openReport(props.childReportID); Navigation.navigate(ROUTES.getReportRoute(props.childReportID)); - return ''; }} > @@ -45,18 +41,15 @@ const ReportActionItemThread = props => ( size={CONST.AVATAR_SIZE.SMALLER} icons={props.icons} shouldStackHorizontally - avatarTooltips={_.map(props.icons, icon => icon.name)} + avatarTooltips={_.map(props.icons, (icon) => icon.name)} /> - + {`${props.numberOfReplies} ${props.numberOfReplies === 1 ? props.translate('threads.reply') : props.translate('threads.replies')}`} - - {`${props.translate('threads.lastReply')} ${props.datetimeToCalendarTime(props.mostRecentReply)}`} - + {`${props.translate('threads.lastReply')} ${props.datetimeToCalendarTime(props.mostRecentReply)}`} - ); diff --git a/src/styles/styles.js b/src/styles/styles.js index 2a5f31e2bca2..a7580acedd53 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -1012,7 +1012,7 @@ const styles = { lineHeight: 16, }, - lhPercent: { + lh140Percent: { lineHeight: '140%', }, From f69af269f64b5e7459c85abb49d07c47b7c6f935 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Thu, 11 May 2023 10:29:54 +0100 Subject: [PATCH 23/28] Remove unused param docs --- src/libs/actions/Report.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index dcfa4ab68d16..26d9b9da7357 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -326,7 +326,6 @@ function addComment(reportID, text) { * @param {String} reportID * @param {Array} participantList The list of users that are included in a new chat, not including the user creating it * @param {Object} newReportObject The optimistic report object created when making a new chat, saved as optimistic data - * @param {String} parentReportActionID The report action that a thread was created off of (only passed for threads) */ function openReport(reportID, participantList = [], newReportObject = {}) { const optimisticReportData = { From 3104e3ec5df0ec42e4ef54d5d78574160951b328 Mon Sep 17 00:00:00 2001 From: Brandon Stites Date: Thu, 11 May 2023 16:42:49 -0600 Subject: [PATCH 24/28] Add beta check --- src/pages/home/report/ReportActionItem.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 7e986c714ee9..b7ca49761ee5 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -49,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 */ @@ -244,7 +245,7 @@ class ReportActionItem extends Component { const reactions = _.get(this.props, ['action', 'message', 0, 'reactions'], []); const hasReactions = reactions.length > 0; - const shouldDisplayThreadReplies = (this.props.action.childCommenterCount || 0) > 0; + const shouldDisplayThreadReplies = (this.props.action.childCommenterCount || 0) > 0 && Permissions.canUseThreads(this.props.betas); const oldestFourEmails = lodashGet(this.props.action, 'childOldestFourEmails', '').split(','); return ( @@ -415,5 +416,8 @@ export default compose( preferredSkinTone: { key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, }, + betas: { + key: ONYXKEYS.BETAS, + }, }), )(ReportActionItem); From f97c60023d0e453157dad615fb1d2ef2741dd724 Mon Sep 17 00:00:00 2001 From: Brandon Stites Date: Thu, 11 May 2023 16:52:06 -0600 Subject: [PATCH 25/28] Add betas to proptypes --- src/pages/home/report/ReportActionItem.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index b7ca49761ee5..3a52836ce935 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -85,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, }; @@ -94,6 +97,7 @@ const defaultProps = { preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, personalDetails: {}, shouldShowSubscriptAvatar: false, + betas: [], }; class ReportActionItem extends Component { From 779fa634576b8dbef620a14827ebcfbb5523bdf7 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Fri, 12 May 2023 12:13:59 +0100 Subject: [PATCH 26/28] Rename to getIconsForParticipants and fix param type --- src/libs/ReportUtils.js | 8 ++++---- src/pages/home/report/ReportActionItem.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 38579379e8b3..2e4dbc0df115 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -687,11 +687,11 @@ function getSmallSizeAvatar(avatarURL, login) { * 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 {Array} participants * @param {Object} personalDetails * @returns {Array<*>} */ -function getIconsFromParticipants(participants, personalDetails) { +function getIconsForParticipants(participants, personalDetails) { const participantDetails = []; const participantsList = participants || []; @@ -801,7 +801,7 @@ function getIcons(report, personalDetails, defaultIcon = null) { ]; } - return getIconsFromParticipants(report.participants, personalDetails); + return getIconsForParticipants(report.participants, personalDetails); } /** @@ -1955,7 +1955,7 @@ export { chatIncludesConcierge, isPolicyExpenseChat, getDefaultAvatar, - getIconsFromParticipants, + getIconsForParticipants, getIcons, getRoomWelcomeMessage, getDisplayNamesWithTooltips, diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 3a52836ce935..cd61292e18be 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -268,7 +268,7 @@ class ReportActionItem extends Component { childReportID={`${this.props.action.childReportID}`} numberOfReplies={this.props.action.childVisibleActionCount || 0} mostRecentReply={`${this.props.action.childLastVisibleActionCreated}`} - icons={ReportUtils.getIconsFromParticipants(oldestFourEmails, this.props.personalDetails)} + icons={ReportUtils.getIconsForParticipants(oldestFourEmails, this.props.personalDetails)} /> )} From 087862ef64a419ddd84752ba3cc75a5181c790ac Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Fri, 12 May 2023 19:55:05 +0100 Subject: [PATCH 27/28] Make text not selectable --- src/components/MultipleAvatars.js | 7 ++++++- src/pages/home/report/ReportActionItemThread.js | 10 ++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) 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/pages/home/report/ReportActionItemThread.js b/src/pages/home/report/ReportActionItemThread.js index 0d51ebc2f56d..9292c4b01c37 100644 --- a/src/pages/home/report/ReportActionItemThread.js +++ b/src/pages/home/report/ReportActionItemThread.js @@ -44,10 +44,16 @@ const ReportActionItemThread = (props) => ( avatarTooltips={_.map(props.icons, (icon) => icon.name)} /> - + {`${props.numberOfReplies} ${props.numberOfReplies === 1 ? props.translate('threads.reply') : props.translate('threads.replies')}`} - {`${props.translate('threads.lastReply')} ${props.datetimeToCalendarTime(props.mostRecentReply)}`} + {`${props.translate('threads.lastReply')} ${props.datetimeToCalendarTime(props.mostRecentReply)}`} From 30f5ebae753b2ce95c53357a29966fd9508ca895 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Fri, 12 May 2023 21:02:05 +0100 Subject: [PATCH 28/28] Don't show on thread preview (first chat in thread) --- src/libs/ReportUtils.js | 12 ++++++++++++ .../home/report/ContextMenu/ContextMenuActions.js | 2 +- src/pages/home/report/ReportActionItem.js | 3 ++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 58310b47dc84..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 @@ -2082,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 cd61292e18be..38b5bda6f713 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -249,7 +249,8 @@ class ReportActionItem extends Component { const reactions = _.get(this.props, ['action', 'message', 0, 'reactions'], []); const hasReactions = reactions.length > 0; - const shouldDisplayThreadReplies = (this.props.action.childCommenterCount || 0) > 0 && Permissions.canUseThreads(this.props.betas); + 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 (