Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,11 @@ function getCurrentUserDisplayNameOrEmail(): string | undefined {
}

function getChatType(report: OnyxInputOrEntry<Report> | Participant): ValueOf<typeof CONST.REPORT.CHAT_TYPE> | 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;
}

Expand Down
7 changes: 5 additions & 2 deletions src/pages/inbox/ReportNavigateAwayHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
Expand Down
64 changes: 64 additions & 0 deletions tests/actions/TaskTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
Loading