diff --git a/src/components/AvatarWithDisplayName.js b/src/components/AvatarWithDisplayName.js index 7889dcb0b703..152eaadcf709 100644 --- a/src/components/AvatarWithDisplayName.js +++ b/src/components/AvatarWithDisplayName.js @@ -17,6 +17,9 @@ import compose from '../libs/compose'; import * as OptionsListUtils from '../libs/OptionsListUtils'; import Text from './Text'; import * as StyleUtils from '../styles/StyleUtils'; +import Navigation from '../libs/Navigation/Navigation'; +import ROUTES from '../ROUTES'; +import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; const propTypes = { /** The report currently being looked at */ @@ -52,6 +55,7 @@ const defaultProps = { function AvatarWithDisplayName(props) { const title = props.isAnonymous ? props.report.displayName : ReportUtils.getDisplayNameForParticipant(props.report.ownerAccountID, true); const subtitle = ReportUtils.getChatRoomSubtitle(props.report); + const parentNavigationSubtitle = ReportUtils.getParentNavigationSubtitle(props.report); const isExpenseReport = ReportUtils.isExpenseReport(props.report); const icons = ReportUtils.getIcons(props.report, props.personalDetails, props.policies); const ownerPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs([props.report.ownerAccountID], props.personalDetails); @@ -88,6 +92,22 @@ function AvatarWithDisplayName(props) { textStyles={[props.isAnonymous ? styles.headerAnonymousFooter : styles.headerText, styles.pre]} shouldUseFullTitle={isExpenseReport || props.isAnonymous} /> + {!_.isEmpty(parentNavigationSubtitle) && ( + { + Navigation.navigate(ROUTES.getReportRoute(props.report.parentReportID)); + }} + accessibilityLabel={subtitle} + accessibilityRole="link" + > + + {parentNavigationSubtitle} + + + )} {!_.isEmpty(subtitle) && ( diff --git a/src/components/MoneyRequestHeader.js b/src/components/MoneyRequestHeader.js index a0e0fc3b70ad..3ecc854951e0 100644 --- a/src/components/MoneyRequestHeader.js +++ b/src/components/MoneyRequestHeader.js @@ -90,6 +90,11 @@ function MoneyRequestHeader(props) { const shouldShowSettlementButton = !isSettled && !props.isSingleTransactionView && isPayer; const bankAccountRoute = ReportUtils.getBankAccountRoute(props.chatReport); const shouldShowPaypal = Boolean(lodashGet(props.personalDetails, [moneyRequestReport.managerID, 'payPalMeAddress'])); + const report = props.report; + if (props.isSingleTransactionView) { + report.ownerAccountID = lodashGet(props, ['parentReport', 'ownerAccountID'], null); + report.ownerEmail = lodashGet(props, ['parentReport', 'ownerEmail'], ''); + } return ( `From ${rootReportName}${workspaceName ? ` in ${workspaceName}` : ''}`, }, qrCodes: { copyUrlToClipboard: 'Copy URL to clipboard', diff --git a/src/languages/es.js b/src/languages/es.js index 27b51cd73492..f765ce0270df 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -1893,7 +1893,7 @@ export default { lastReply: 'Ăšltima respuesta', replies: 'Respuestas', reply: 'Respuesta', - from: 'De', + parentNavigationSummary: ({rootReportName, workspaceName}) => `De ${rootReportName}${workspaceName ? ` en ${workspaceName}` : ''}`, }, qrCodes: { copyUrlToClipboard: 'Copiar URL al portapapeles', diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 72f8f297344e..aa3f600cb9d7 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -461,7 +461,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { result.isArchivedRoom = ReportUtils.isArchivedRoom(report); result.isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); result.isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); - result.isThread = ReportUtils.isThread(report); + result.isThread = ReportUtils.isChatThread(report); result.isTaskReport = ReportUtils.isTaskReport(report); result.shouldShowSubscript = ReportUtils.shouldReportShowSubscript(report); result.allReportErrors = getAllReportErrors(report, reportActions); @@ -644,7 +644,7 @@ function getOptions( return; } - const isThread = ReportUtils.isThread(report); + const isThread = ReportUtils.isChatThread(report); const isChatRoom = ReportUtils.isChatRoom(report); const isTaskReport = ReportUtils.isTaskReport(report); const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 4ddbf032867c..5de8abb15ed6 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -378,13 +378,23 @@ function getBankAccountRoute(report) { } /** - * Returns true if report has a parent and is therefore a Thread. + * Returns true if report has a parent * * @param {Object} report * @returns {Boolean} */ function isThread(report) { - return Boolean(report && report.parentReportID && report.parentReportActionID && report.type === CONST.REPORT.TYPE.CHAT); + return Boolean(report && report.parentReportID && report.parentReportActionID); +} + +/** + * Returns true if report is of type chat and has a parent and is therefore a Thread. + * + * @param {Object} report + * @returns {Boolean} + */ +function isChatThread(report) { + return isThread(report) && report.type === CONST.REPORT.TYPE.CHAT; } /** @@ -394,7 +404,7 @@ function isThread(report) { * @returns {Boolean} */ function isConciergeChatReport(report) { - return lodashGet(report, 'participantAccountIDs', []).length === 1 && Number(report.participantAccountIDs[0]) === CONST.ACCOUNT_ID.CONCIERGE && !isThread(report); + return lodashGet(report, 'participantAccountIDs', []).length === 1 && Number(report.participantAccountIDs[0]) === CONST.ACCOUNT_ID.CONCIERGE && !isChatThread(report); } /** @@ -480,9 +490,15 @@ function isArchivedRoom(report) { * @param {String} report.policyID * @param {String} report.oldPolicyName * @param {String} report.policyName + * @param {Boolean} [returnEmptyIfNotFound] * @returns {String} */ -function getPolicyName(report) { +function getPolicyName(report, returnEmptyIfNotFound = false) { + const noPolicyFound = returnEmptyIfNotFound ? '' : Localize.translateLocal('workspace.common.unavailable'); + if (report === undefined) { + return noPolicyFound; + } + if ((!allPolicies || _.size(allPolicies) === 0) && !report.policyName) { return Localize.translateLocal('workspace.common.unavailable'); } @@ -491,7 +507,7 @@ function getPolicyName(report) { // // Public rooms send back the policy name with the reportSummary, // // since they can also be accessed by people who aren't in the workspace - return lodashGet(policy, 'name') || report.policyName || report.oldPolicyName || Localize.translateLocal('workspace.common.unavailable'); + return lodashGet(policy, 'name') || report.policyName || report.oldPolicyName || noPolicyFound; } /** @@ -734,7 +750,7 @@ function getIcons(report, personalDetails, defaultIcon = null, isPayer = false, result.source = Expensicons.DeletedRoomAvatar; return [result]; } - if (isThread(report)) { + if (isChatThread(report)) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); const actorEmail = lodashGet(parentReportAction, 'actorEmail', ''); @@ -1030,7 +1046,7 @@ function getReportPreviewMessage(report, reportAction) { */ function getReportName(report) { let formattedName; - if (isThread(report)) { + if (isChatThread(report)) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); if (ReportActionsUtils.isTransactionThread(parentReportAction)) { return getTransactionReportName(parentReportAction); @@ -1072,17 +1088,33 @@ function getReportName(report) { } /** - * Recursively navigates through parent to get the root reports name only for DM reports. + * Recursively navigates through thread parents to get the root report and workspace name. + * The recursion stops when we find a non thread or money request report, whichever comes first. * @param {Object} report - * @returns {String|*} + * @returns {Object} */ -function getDMRootReportName(report) { - if (isThread(report) && !getChatType(report)) { +function getRootReportAndWorkspaceName(report) { + if (isThread(report) && !isMoneyRequestReport(report)) { const parentReport = lodashGet(allReports, [`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`]); - return getDMRootReportName(parentReport); + return getRootReportAndWorkspaceName(parentReport); + } + + if (isIOUReport(report)) { + return { + rootReportName: lodashGet(report, 'displayName', ''), + }; + } + if (isMoneyRequestReport(report)) { + return { + rootReportName: lodashGet(report, 'displayName', ''), + workspaceName: isIOUReport(report) ? CONST.POLICY.OWNER_EMAIL_FAKE : getPolicyName(report, true), + }; } - return getReportName(report); + return { + rootReportName: getReportName(report), + workspaceName: getPolicyName(report, true), + }; } /** @@ -1091,23 +1123,8 @@ function getDMRootReportName(report) { * @returns {String} */ function getChatRoomSubtitle(report) { - if (isThread(report)) { - if (!getChatType(report)) { - return `${Localize.translateLocal('threads.from')} ${getDMRootReportName(report)}`; - } - - let roomName = ''; - if (isChatRoom(report)) { - const parentReport = lodashGet(allReports, [`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`]); - if (parentReport) { - roomName = lodashGet(parentReport, 'displayName', ''); - } else { - roomName = lodashGet(report, 'displayName', ''); - } - } - - const workspaceName = getPolicyName(report); - return `${Localize.translateLocal('threads.from')} ${roomName ? [roomName, workspaceName].join(' in ') : workspaceName}`; + if (isChatThread(report)) { + return ''; } if (!isDefaultRoom(report) && !isUserCreatedPolicyRoom(report) && !isPolicyExpenseChat(report)) { return ''; @@ -1125,6 +1142,24 @@ function getChatRoomSubtitle(report) { return getPolicyName(report); } +/** + * Gets the parent navigation subtitle for the report + * @param {Object} report + * @returns {String} + */ +function getParentNavigationSubtitle(report) { + if (isThread(report)) { + const parentReport = lodashGet(allReports, [`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`]); + const {rootReportName, workspaceName} = getRootReportAndWorkspaceName(parentReport); + if (_.isEmpty(rootReportName)) { + return ''; + } + + return Localize.translateLocal('threads.parentNavigationSummary', {rootReportName, workspaceName}); + } + return ''; +} + /** * Get the report for a reportID * @@ -1143,7 +1178,7 @@ function getReport(reportID) { function navigateToDetailsPage(report) { const participantAccountIDs = lodashGet(report, 'participantAccountIDs', []); - if (isChatRoom(report) || isPolicyExpenseChat(report) || isThread(report)) { + if (isChatRoom(report) || isPolicyExpenseChat(report) || isChatThread(report)) { Navigation.navigate(ROUTES.getReportDetailsRoute(report.reportID)); return; } @@ -1961,7 +1996,7 @@ function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, iouRep if ( !report || !report.reportID || - (_.isEmpty(report.participantAccountIDs) && !isThread(report) && !isPublicRoom(report) && !isArchivedRoom(report) && !isMoneyRequestReport(report) && !isTaskReport(report)) + (_.isEmpty(report.participantAccountIDs) && !isChatThread(report) && !isPublicRoom(report) && !isArchivedRoom(report) && !isMoneyRequestReport(report) && !isTaskReport(report)) ) { return false; } @@ -2021,7 +2056,7 @@ function getChatByParticipants(newParticipantList) { newParticipantList.sort(); return _.find(allReports, (report) => { // If the report has been deleted, or there are no participants (like an empty #admins room) then skip it - if (!report || !report.participantAccountIDs || isThread(report)) { + if (!report || !report.participantAccountIDs || isChatThread(report)) { return false; } @@ -2201,7 +2236,7 @@ function canRequestMoney(report) { */ function getMoneyRequestOptions(report, reportParticipants, betas) { // In any thread, we do not allow any new money requests yet - if (isThread(report)) { + if (isChatThread(report)) { return []; } @@ -2307,7 +2342,7 @@ function shouldReportShowSubscript(report) { return false; } - if (isPolicyExpenseChat(report) && !isThread(report) && !isTaskReport(report) && !report.isOwnPolicyExpenseChat) { + if (isPolicyExpenseChat(report) && !isChatThread(report) && !isTaskReport(report) && !report.isOwnPolicyExpenseChat) { return true; } @@ -2372,6 +2407,7 @@ export { isUserCreatedPolicyRoom, isChatRoom, getChatRoomSubtitle, + getParentNavigationSubtitle, getPolicyName, getPolicyType, isArchivedRoom, @@ -2440,6 +2476,7 @@ export { getWhisperDisplayNames, getWorkspaceAvatar, isThread, + isChatThread, isThreadParent, isThreadFirstChat, shouldReportShowSubscript, diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js index ee53cd9df69a..f722f0cd1572 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.js @@ -263,7 +263,7 @@ function getOptionData(reportID) { const participantPersonalDetailList = _.values(OptionsListUtils.getPersonalDetailsForAccountIDs(report.participantAccountIDs, personalDetails)); const personalDetail = participantPersonalDetailList[0] || {}; - result.isThread = ReportUtils.isThread(report); + result.isThread = ReportUtils.isChatThread(report); result.isChatRoom = ReportUtils.isChatRoom(report); result.isTaskReport = ReportUtils.isTaskReport(report); if (result.isTaskReport) { diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js index 59a84182b803..3d19a632a634 100644 --- a/src/libs/actions/Task.js +++ b/src/libs/actions/Task.js @@ -57,6 +57,7 @@ function createTaskAndNavigate(currentUserEmail, currentUserAccountID, parentRep // Create the CreatedReportAction on the task const optimisticTaskCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(optimisticTaskReport.reportID); const optimisticAddCommentReport = ReportUtils.buildOptimisticTaskCommentReportAction(taskReportID, title, assignee, assigneeAccountID, `Created a task: ${title}`, parentReportID); + optimisticTaskReport.parentReportActionID = optimisticAddCommentReport.reportAction.reportActionID; const currentTime = DateUtils.getDBTime(); diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js index 26588481f53a..cb727d9e3eae 100644 --- a/src/pages/ReportDetailsPage.js +++ b/src/pages/ReportDetailsPage.js @@ -63,7 +63,7 @@ function ReportDetailsPage(props) { const isPolicyAdmin = useMemo(() => PolicyUtils.isPolicyAdmin(policy), [policy]); const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(props.report), [props.report]); const isChatRoom = useMemo(() => ReportUtils.isChatRoom(props.report), [props.report]); - const isThread = useMemo(() => ReportUtils.isThread(props.report), [props.report]); + const isThread = useMemo(() => ReportUtils.isChatThread(props.report), [props.report]); const isUserCreatedPolicyRoom = useMemo(() => ReportUtils.isUserCreatedPolicyRoom(props.report), [props.report]); const isArchivedRoom = useMemo(() => ReportUtils.isArchivedRoom(props.report), [props.report]); diff --git a/src/pages/ReportParticipantsPage.js b/src/pages/ReportParticipantsPage.js index cdcaabcfe34d..6d9bdbffba54 100755 --- a/src/pages/ReportParticipantsPage.js +++ b/src/pages/ReportParticipantsPage.js @@ -93,7 +93,9 @@ function ReportParticipantsPage(props) { 1; const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(participantPersonalDetails, isMultipleParticipant); - const isThread = ReportUtils.isThread(props.report); + const isChatThread = ReportUtils.isChatThread(props.report); const isChatRoom = ReportUtils.isChatRoom(props.report); const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(props.report); const isTaskReport = ReportUtils.isTaskReport(props.report); - const reportHeaderData = !isTaskReport && !isThread && props.report.parentReportID ? props.parentReport : props.report; + const reportHeaderData = !isTaskReport && !isChatThread && props.report.parentReportID ? props.parentReport : props.report; const title = ReportUtils.getReportName(reportHeaderData); const subtitle = ReportUtils.getChatRoomSubtitle(reportHeaderData); + const parentNavigationSubtitle = ReportUtils.getParentNavigationSubtitle(reportHeaderData); const isConcierge = participants.length === 1 && _.contains(participants, CONST.ACCOUNT_ID.CONCIERGE); const isAutomatedExpensifyAccount = participants.length === 1 && ReportUtils.hasAutomatedExpensifyAccountIDs(participants); const guideCalendarLink = lodashGet(props.account, 'guideCalendarLink'); @@ -174,7 +175,7 @@ function HeaderView(props) { ) : ( )} @@ -184,35 +185,31 @@ function HeaderView(props) { tooltipEnabled numberOfLines={1} textStyles={[styles.headerText, styles.pre]} - shouldUseFullTitle={isChatRoom || isPolicyExpenseChat || isThread || isTaskReport} + shouldUseFullTitle={isChatRoom || isPolicyExpenseChat || isChatThread || isTaskReport} /> - {(isChatRoom || isPolicyExpenseChat || isThread) && !_.isEmpty(subtitle) && ( - <> - {isThread ? ( - { - Navigation.navigate(ROUTES.getReportRoute(props.report.parentReportID)); - }} - style={[styles.alignSelfStart, styles.mw100]} - accessibilityLabel={subtitle} - accessibilityRole="link" - > - - {subtitle} - - - ) : ( - - {subtitle} - - )} - + {!_.isEmpty(parentNavigationSubtitle) && ( + { + Navigation.navigate(ROUTES.getReportRoute(props.report.parentReportID)); + }} + accessibilityLabel={parentNavigationSubtitle} + accessibilityRole="link" + > + + {parentNavigationSubtitle} + + + )} + {!_.isEmpty(subtitle) && ( + + {subtitle} + )} {brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR && ( diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 8e48f06c3528..b0f108ed7d39 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -116,7 +116,7 @@ function ReportActionsList(props) { ({item: reportAction, index}) => { // When the new indicator should not be displayed we explicitly set it to null const shouldDisplayNewMarker = reportAction.reportActionID === newMarkerReportActionID; - const shouldDisplayParentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED && ReportUtils.isThread(report); + const shouldDisplayParentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED && ReportUtils.isChatThread(report); const shouldHideThreadDividerLine = shouldDisplayParentAction && sortedReportActions.length > 1 && sortedReportActions[sortedReportActions.length - 2].reportActionID === newMarkerReportActionID; return shouldDisplayParentAction ? ( diff --git a/src/pages/settings/Report/ReportSettingsPage.js b/src/pages/settings/Report/ReportSettingsPage.js index 4c8f82c625cd..02ab518d999b 100644 --- a/src/pages/settings/Report/ReportSettingsPage.js +++ b/src/pages/settings/Report/ReportSettingsPage.js @@ -92,9 +92,9 @@ class ReportSettingsPage extends Component { } render() { - const shouldShowRoomName = !ReportUtils.isPolicyExpenseChat(this.props.report) && !ReportUtils.isThread(this.props.report); + const shouldShowRoomName = !ReportUtils.isPolicyExpenseChat(this.props.report) && !ReportUtils.isChatThread(this.props.report); const linkedWorkspace = _.find(this.props.policies, (policy) => policy && policy.id === this.props.report.policyID); - const shouldDisableRename = this.shouldDisableRename(linkedWorkspace) || ReportUtils.isThread(this.props.report); + const shouldDisableRename = this.shouldDisableRename(linkedWorkspace) || ReportUtils.isChatThread(this.props.report); const notificationPreference = this.props.translate(`notificationPreferencesPage.notificationPreferences.${this.props.report.notificationPreference}`); const shouldDisableWelcomeMessage = this.shouldDisableWelcomeMessage(linkedWorkspace); const writeCapability = this.props.report.writeCapability || CONST.REPORT.WRITE_CAPABILITIES.ALL;