diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9b3d8ca23dc0..2f995e578e43 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1245,6 +1245,11 @@ function getCurrentUserDisplayNameOrEmail(): string | undefined { } function getChatType(report: OnyxInputOrEntry | Participant): ValueOf | undefined { + // Tasks only support the #admins chatType inheritance (see buildOptimisticTaskReport). The backend can also + // inherit other parent chat types (e.g. `group`), which would misclassify the task as that chat type. + if (report && 'type' in report && report.type === CONST.REPORT.TYPE.TASK && report.chatType !== CONST.REPORT.CHAT_TYPE.POLICY_ADMINS) { + return undefined; + } return report?.chatType; } diff --git a/src/pages/inbox/ReportNavigateAwayHandler.tsx b/src/pages/inbox/ReportNavigateAwayHandler.tsx index 85fd839b685a..d78964cd36c3 100644 --- a/src/pages/inbox/ReportNavigateAwayHandler.tsx +++ b/src/pages/inbox/ReportNavigateAwayHandler.tsx @@ -10,7 +10,7 @@ import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import {isDeletedParentAction} from '@libs/ReportActionsUtils'; -import {isAdminRoom, isAnnounceRoom, isGroupChat, isMoneyRequest, isMoneyRequestReport, isMoneyRequestReportPendingDeletion, isPolicyExpenseChat} from '@libs/ReportUtils'; +import {isAdminRoom, isAnnounceRoom, isGroupChat, isMoneyRequest, isMoneyRequestReport, isMoneyRequestReportPendingDeletion, isPolicyExpenseChat, isTaskReport} from '@libs/ReportUtils'; import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList} from '@navigation/types'; @@ -166,7 +166,10 @@ function ReportNavigateAwayHandler() { (isPolicyExpenseChat(prevReport) && !prevReport?.isOwnPolicyExpenseChat) || isGroupChat(prevReport) || isAdminRoom(prevReport) || - isAnnounceRoom(prevReport)); + isAnnounceRoom(prevReport) || + // Tasks no longer classify as their parent's chat type (see getChatType), so removal + // must be expected for the task type itself to keep the Concierge redirect working. + isTaskReport(prevReport)); const didReportClose = wasReportRemoved && prevReport.statusNum === CONST.REPORT.STATUS_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED; const isTopLevelPolicyRoomWithNoStatus = !report?.statusNum && !prevReport?.parentReportID && prevReport?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ROOM; const isClosedTopLevelPolicyRoom = wasReportRemoved && prevReport.statusNum === CONST.REPORT.STATUS_NUM.OPEN && isTopLevelPolicyRoomWithNoStatus; diff --git a/tests/actions/TaskTest.ts b/tests/actions/TaskTest.ts index 2895d9473ec5..e712c9457cb2 100644 --- a/tests/actions/TaskTest.ts +++ b/tests/actions/TaskTest.ts @@ -1789,4 +1789,68 @@ describe('actions/Task', () => { expect(assignee?.displayName).toBe('HiddenMarker'); }); }); + + describe('task chatType classification (#96421)', () => { + it('does not treat a task that inherited a non-admins chatType as a group chat', () => { + // Given a task report the backend returned carrying the parent group chat's chatType + const groupTask = { + reportID: 'task_group_1', + type: CONST.REPORT.TYPE.TASK, + chatType: CONST.REPORT.CHAT_TYPE.GROUP, + parentReportID: 'group_parent_1', + parentReportActionID: 'group_parent_action_1', + } as Report; + + // Then it is not classified as a group chat or a root group chat, so the destructive leave path is never offered + expect(ReportUtils.isGroupChat(groupTask)).toBe(false); + expect(ReportUtils.isRootGroupChat(groupTask)).toBe(false); + expect(ReportUtils.canLeaveChat(groupTask, undefined, 1)).toBe(false); + }); + + it('does not treat a task that inherited a policy room chatType as a chat room', () => { + // Given a task report carrying a user-created policy room's chatType + const viewerAccountID = 1; + const roomTask = { + reportID: 'task_room_1', + type: CONST.REPORT.TYPE.TASK, + chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, + parentReportID: 'room_parent_1', + parentReportActionID: 'room_parent_action_1', + // A visible participant so the canLeaveChat assertion discriminates on the classification, not on hidden-membership + participants: {[viewerAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}}, + } as Report; + + // Then it is not classified as a chat room, so room-only actions (like Leave) stay hidden + expect(ReportUtils.isChatRoom(roomTask)).toBe(false); + expect(ReportUtils.canLeaveChat(roomTask, undefined, viewerAccountID)).toBe(false); + }); + + it('keeps the intentional #admins chatType inheritance for tasks', () => { + // Given a task created under an #admins room (the one inheritance the client supports) + const adminsTask = { + reportID: 'task_admins_1', + type: CONST.REPORT.TYPE.TASK, + chatType: CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, + parentReportID: 'admins_parent_1', + parentReportActionID: 'admins_parent_action_1', + } as Report; + + // Then the admins chatType is preserved: it still reads as an admin room, never as a group chat + expect(ReportUtils.isAdminRoom(adminsTask)).toBe(true); + expect(ReportUtils.isGroupChat(adminsTask)).toBe(false); + }); + + it('still classifies a real group chat as a group chat', () => { + // Given an actual group chat (type CHAT), which the sanitization must not affect + const groupChat = { + reportID: 'group_chat_1', + type: CONST.REPORT.TYPE.CHAT, + chatType: CONST.REPORT.CHAT_TYPE.GROUP, + } as Report; + + // Then it is still classified as a group chat and a root group chat + expect(ReportUtils.isGroupChat(groupChat)).toBe(true); + expect(ReportUtils.isRootGroupChat(groupChat)).toBe(true); + }); + }); });