From 38a9db21e330020a38dd2c2eb12525652f863811 Mon Sep 17 00:00:00 2001 From: MobileMage Date: Sat, 25 Jul 2026 01:11:11 +0100 Subject: [PATCH 1/4] Prevent tasks from inheriting non-admins chatType classification A task inside a group chat is returned by the backend carrying the parent's chatType (e.g. group). getChatType now drops that inherited value for non-#admins tasks, so isGroupChat/isRootGroupChat/canLeaveChat no longer misclassify the task, the bogus Leave row and destructive leaveGroupChat path disappear, and the details page stops flashing the Not-here page. Keeps the intentional POLICY_ADMINS inheritance. Fixes #96421. --- src/libs/ReportUtils.ts | 5 +++++ tests/actions/TaskTest.ts | 45 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) 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/tests/actions/TaskTest.ts b/tests/actions/TaskTest.ts index 2895d9473ec5..ec09f8d2b9a1 100644 --- a/tests/actions/TaskTest.ts +++ b/tests/actions/TaskTest.ts @@ -1789,4 +1789,49 @@ 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); + }); + + 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); + }); + }); }); From 1920401b97e4e1792bd37d008deaa068c50d940b Mon Sep 17 00:00:00 2001 From: MobileMage Date: Sat, 25 Jul 2026 03:02:51 +0100 Subject: [PATCH 2/4] Keep Concierge redirect for removed tasks and pin leave surface in tests Tasks no longer classify as their parent's chat type, so a wiped group-inherited task stopped matching isGroupChat in isRemovalExpectedForReportType and the user would stay on the Not-here page instead of redirecting. Treat task removals as expected for the task type itself. Also pin canLeaveChat(task) === false and add a policy-room-inherited task case to the classification tests. --- src/pages/inbox/ReportNavigateAwayHandler.tsx | 7 +++++-- tests/actions/TaskTest.ts | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) 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 ec09f8d2b9a1..349978c74d01 100644 --- a/tests/actions/TaskTest.ts +++ b/tests/actions/TaskTest.ts @@ -1804,6 +1804,22 @@ describe('actions/Task', () => { // 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 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', + } as Report; + + // Then it is not classified as a chat room, so room-only affordances (like Leave) stay hidden + expect(ReportUtils.isChatRoom(roomTask)).toBe(false); + expect(ReportUtils.canLeaveChat(roomTask, undefined, 1)).toBe(false); }); it('keeps the intentional #admins chatType inheritance for tasks', () => { From 12cab55fa42012d7e82808c906fc43b73c4977be Mon Sep 17 00:00:00 2001 From: MobileMage Date: Sat, 25 Jul 2026 03:31:58 +0100 Subject: [PATCH 3/4] Reword test comment to satisfy spellcheck --- tests/actions/TaskTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/actions/TaskTest.ts b/tests/actions/TaskTest.ts index 349978c74d01..57464c52b583 100644 --- a/tests/actions/TaskTest.ts +++ b/tests/actions/TaskTest.ts @@ -1817,7 +1817,7 @@ describe('actions/Task', () => { parentReportActionID: 'room_parent_action_1', } as Report; - // Then it is not classified as a chat room, so room-only affordances (like Leave) stay hidden + // 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, 1)).toBe(false); }); From 17f26677abfe35ff17f5ca5c18ee8f487b16090d Mon Sep 17 00:00:00 2001 From: MobileMage Date: Sat, 25 Jul 2026 13:58:16 +0100 Subject: [PATCH 4/4] Make the room-task canLeaveChat assertion discriminating Give the fixture a visible participant so the assertion exercises the classification path rather than passing through hidden-membership. --- tests/actions/TaskTest.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/actions/TaskTest.ts b/tests/actions/TaskTest.ts index 57464c52b583..e712c9457cb2 100644 --- a/tests/actions/TaskTest.ts +++ b/tests/actions/TaskTest.ts @@ -1809,17 +1809,20 @@ describe('actions/Task', () => { 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, 1)).toBe(false); + expect(ReportUtils.canLeaveChat(roomTask, undefined, viewerAccountID)).toBe(false); }); it('keeps the intentional #admins chatType inheritance for tasks', () => {