diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5749e241b6fb..9017ccdba96a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5522,6 +5522,40 @@ function temporary_getMoneyRequestOptions( return getMoneyRequestOptions(report, policy, reportParticipants, canUseTrackExpense, true) as Array>; } +/** + * Invoice sender, invoice receiver and auto-invited admins cannot leave + */ +function canLeaveInvoiceRoom(report: OnyxEntry): boolean { + if (!isInvoiceRoom(report)) { + return false; + } + + const invoiceReport = getReport(report?.iouReportID ?? ''); + + if (invoiceReport?.ownerAccountID === currentUserAccountID) { + return false; + } + + if (invoiceReport?.managerID === currentUserAccountID) { + return false; + } + + const isSenderPolicyAdmin = getPolicy(report?.policyID)?.role === CONST.POLICY.ROLE.ADMIN; + + if (isSenderPolicyAdmin) { + return false; + } + + const isReceiverPolicyAdmin = + report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS ? getPolicy(report?.invoiceReceiver?.policyID)?.role === CONST.POLICY.ROLE.ADMIN : false; + + if (isReceiverPolicyAdmin) { + return false; + } + + return true; +} + /** * Allows a user to leave a policy room according to the following conditions of the visibility or chatType rNVP: * `public` - Anyone can leave (because anybody can join) @@ -5984,6 +6018,13 @@ function isDeprecatedGroupDM(report: OnyxEntry): boolean { ); } +/** + * A "root" group chat is the top level group chat and does not refer to any threads off of a Group Chat + */ +function isRootGroupChat(report: OnyxEntry): boolean { + return !isChatThread(report) && (isGroupChat(report) || isDeprecatedGroupDM(report)); +} + /** * Assume any report without a reportID is unusable. */ @@ -6400,10 +6441,63 @@ function hasActionsWithErrors(reportID: string): boolean { return Object.values(reportActions).some((action) => !isEmptyObject(action.errors)); } -function canLeavePolicyExpenseChat(report: OnyxEntry, policy: OnyxEntry): boolean { +function isNonAdminOrOwnerOfPolicyExpenseChat(report: OnyxEntry, policy: OnyxEntry): boolean { return isPolicyExpenseChat(report) && !(PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPolicyOwner(policy, currentUserAccountID ?? -1) || isReportOwner(report)); } +/** + * Whether the user can join a report + */ +function canJoinChat(report: OnyxEntry, parentReportAction: OnyxEntry, policy: OnyxEntry): boolean { + // We disabled thread functions for whisper action + // So we should not show join option for existing thread on whisper message that has already been left, or manually leave it + if (ReportActionsUtils.isWhisperAction(parentReportAction)) { + return false; + } + + // If the notification preference of the chat is not hidden that means we have already joined the chat + if (report?.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) { + return false; + } + + // Anyone viewing these chat types is already a participant and therefore cannot join + if (isRootGroupChat(report) || isSelfDM(report) || isInvoiceRoom(report)) { + return false; + } + + // The user who is a member of the workspace has already joined the public announce room. + if (isPublicAnnounceRoom(report) && !isEmptyObject(policy)) { + return false; + } + + return isChatThread(report) || isUserCreatedPolicyRoom(report) || isNonAdminOrOwnerOfPolicyExpenseChat(report, policy); +} + +/** + * Whether the user can leave a report + */ +function canLeaveChat(report: OnyxEntry, policy: OnyxEntry): boolean { + if (report?.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) { + return false; + } + + // Anyone viewing these chat types is already a participant and therefore cannot leave + if (isSelfDM(report) || isRootGroupChat(report)) { + return false; + } + + // The user who is a member of the workspace cannot leave the public announce room. + if (isPublicAnnounceRoom(report) && !isEmptyObject(policy)) { + return false; + } + + if (canLeaveInvoiceRoom(report)) { + return true; + } + + return (isChatThread(report) && !!report?.notificationPreference?.length) || isUserCreatedPolicyRoom(report) || isNonAdminOrOwnerOfPolicyExpenseChat(report, policy); +} + function getReportActionActorAccountID(reportAction: OnyxEntry, iouReport: OnyxEntry | undefined): number | undefined { switch (reportAction?.actionName) { case CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW: @@ -6559,8 +6653,10 @@ export { canEditRoomVisibility, canEditWriteCapability, canFlagReportAction, - canLeavePolicyExpenseChat, + isNonAdminOrOwnerOfPolicyExpenseChat, canLeaveRoom, + canJoinChat, + canLeaveChat, canReportBeMentionedWithinPolicy, canRequestMoney, canSeeDefaultRoom, @@ -6688,6 +6784,7 @@ export { isDM, isDefaultRoom, isDeprecatedGroupDM, + isRootGroupChat, isExpenseReport, isExpenseRequest, isExpensifyOnlyParticipantInReport, diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx index 935572a1e7dd..c0111c6f85e7 100644 --- a/src/pages/home/HeaderView.tsx +++ b/src/pages/home/HeaderView.tsx @@ -24,7 +24,6 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as HeaderUtils from '@libs/HeaderUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as Link from '@userActions/Link'; import * as Report from '@userActions/Report'; @@ -106,13 +105,9 @@ function HeaderView({ const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(reportHeaderData); const isConcierge = ReportUtils.hasSingleParticipant(report) && participants.includes(CONST.ACCOUNT_ID.CONCIERGE); const isCanceledTaskReport = ReportUtils.isCanceledTaskReport(report, parentReportAction); - const isWhisperAction = ReportActionsUtils.isWhisperAction(parentReportAction); - const isUserCreatedPolicyRoom = ReportUtils.isUserCreatedPolicyRoom(report); const isPolicyEmployee = useMemo(() => !isEmptyObject(policy), [policy]); - const canLeaveRoom = ReportUtils.canLeaveRoom(report, isPolicyEmployee); const reportDescription = ReportUtils.getReportDescriptionText(report); const policyName = ReportUtils.getPolicyName(report, true); - const canLeavePolicyExpenseChat = ReportUtils.canLeavePolicyExpenseChat(report, policy); const policyDescription = ReportUtils.getPolicyDescriptionText(policy); const isPersonalExpenseChat = isPolicyExpenseChat && ReportUtils.isCurrentUserSubmitter(report.reportID); const shouldShowSubtitle = () => { @@ -157,16 +152,14 @@ function HeaderView({ Report.updateNotificationPreference(reportID, report.notificationPreference, CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, false, report.parentReportID, report.parentReportActionID), ); - const canJoinOrLeave = !isSelfDM && !isGroupChat && (isChatThread || isUserCreatedPolicyRoom || canLeaveRoom || canLeavePolicyExpenseChat); - const canJoin = canJoinOrLeave && !isWhisperAction && report.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; - const canLeave = canJoinOrLeave && ((isChatThread && !!report.notificationPreference?.length) || isUserCreatedPolicyRoom || canLeaveRoom || canLeavePolicyExpenseChat); + const canJoin = ReportUtils.canJoinChat(report, parentReportAction, policy); if (canJoin) { threeDotMenuItems.push({ icon: Expensicons.ChatBubbles, text: translate('common.join'), onSelected: join, }); - } else if (canLeave) { + } else if (ReportUtils.canLeaveChat(report, policy)) { const isWorkspaceMemberLeavingWorkspaceRoom = !isChatThread && (report.visibility === CONST.REPORT.VISIBILITY.RESTRICTED || isPolicyExpenseChat) && isPolicyEmployee; threeDotMenuItems.push({ icon: Expensicons.ChatBubbles,