Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e389f17
fix header
M00rish Apr 21, 2026
fbfb102
re-add fix
M00rish Apr 21, 2026
d7e432c
updates picked
M00rish Apr 30, 2026
aba1f01
Merge branch 'Expensify:main' into invitecash
M00rish May 4, 2026
3ccece2
lint
M00rish May 4, 2026
ef2fe71
fixes based on review
M00rish Jun 3, 2026
1b88ca5
fix conflict
M00rish Jun 16, 2026
d0ef64b
gate invite page
M00rish Jun 18, 2026
8f24eb8
Merge branch 'Expensify:main' into invitecash
M00rish Jun 18, 2026
3ff45cc
fix tests
M00rish Jun 19, 2026
7a3bb59
Merge branch 'invitecash' of https://github.com/M00rish/App into invi…
M00rish Jun 19, 2026
295809c
remove duplicate consts
M00rish Jun 19, 2026
4c18664
review fix
M00rish Jun 24, 2026
73183b9
Merge branch 'main' into invitecash
M00rish Jul 7, 2026
cc7e06b
resolve conflicts
M00rish Jul 7, 2026
6e49efa
limit invite to submitter and admin
M00rish Jul 9, 2026
9eba2e9
eslint
M00rish Jul 9, 2026
e83bbe8
resolve conflict
M00rish Jul 13, 2026
07790da
gate for chat admins too
M00rish Jul 13, 2026
e440411
Merge branch 'Expensify:main' into invitecash
M00rish Jul 13, 2026
057c131
fix conflicts
M00rish Jul 23, 2026
f2c02e9
Merge branch 'invitecash' of https://github.com/M00rish/App into invi…
M00rish Jul 23, 2026
849a348
Merge branch 'main' into invitecash
M00rish Jul 23, 2026
50c0bc0
fix
M00rish Jul 23, 2026
4bc82c7
lint
M00rish Jul 23, 2026
b326c17
lint
M00rish Jul 23, 2026
616304d
lint
M00rish Jul 25, 2026
6f1c9c1
Merge branch 'main' into invitecash
M00rish Jul 26, 2026
054241c
review updates
M00rish Jul 27, 2026
12358e3
lint
M00rish Jul 27, 2026
ff41852
restore original resolveOpenReportDuplicationConflictAction
M00rish Jul 27, 2026
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
30 changes: 25 additions & 5 deletions src/pages/DynamicReportParticipantsInvitePage.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
Expand All @@ -11,6 +12,8 @@
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import usePersonalDetailSearchSelector from '@hooks/usePersonalDetailSearchSelector';
import usePolicy from '@hooks/usePolicy';
import useReportAttributes from '@hooks/useReportAttributes';
import useThemeStyles from '@hooks/useThemeStyles';

import {inviteToGroupChat, searchUserInServer} from '@libs/actions/Report';
Expand All @@ -21,8 +24,9 @@
import {getHeaderMessage} from '@libs/PersonalDetailOptionsListUtils';
import type {OptionData} from '@libs/PersonalDetailOptionsListUtils';
import {addSMSDomainIfPhoneNumber, parsePhoneNumber} from '@libs/PhoneNumber';
import {getGroupChatName} from '@libs/ReportNameUtils';
import {getParticipantsAccountIDsForDisplay} from '@libs/ReportUtils';
import {getReportName} from '@libs/ReportNameUtils';
import {getParticipantsAccountIDsForDisplay, isCurrentUserSubmitter, isGroupChat, isGroupChatAdmin, isMoneyRequestReport, isOpenExpenseReport, isPolicyAdmin} from '@libs/ReportUtils';
import StringUtils from '@libs/StringUtils';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand All @@ -31,7 +35,7 @@
import type {InvitedEmailsToAccountIDs} from '@src/types/onyx';
import getEmptyArray from '@src/types/utils/getEmptyArray';

import React, {useEffect, useState} from 'react';
import {useEffect, useState} from 'react';

import type {WithReportOrNotFoundProps} from './inbox/report/withReportOrNotFound';

Expand All @@ -50,6 +54,12 @@
});
const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false);
const backPath = useDynamicBackPath(DYNAMIC_ROUTES.REPORT_PARTICIPANTS_INVITE.path);
const [session] = useOnyx(ONYXKEYS.SESSION);
const currentUserAccountID = Number(session?.accountID);
const policy = usePolicy(report.policyID);
const isReportSubmitterOrAdmin = isCurrentUserSubmitter(report) || isPolicyAdmin(policy) || (isGroupChat(report) && isGroupChatAdmin(report, currentUserAccountID));
const shouldShowInvitePage = isReportSubmitterOrAdmin && (isGroupChat(report) || (isMoneyRequestReport(report) && isOpenExpenseReport(report)));
const reportAttributes = useReportAttributes();

// Any existing participants and Expensify emails should not be eligible for invitation
const excludedUsers: Record<string, boolean> = {
Expand Down Expand Up @@ -115,7 +125,7 @@
toggleSelection(option);
};

const reportName = getGroupChatName(formatPhoneNumber, translate, undefined, true, report);
const reportName = StringUtils.lineBreaksToSpaces(getReportName(report, reportAttributes));

Check failure on line 128 in src/pages/DynamicReportParticipantsInvitePage.tsx

View workflow job for this annotation

GitHub Actions / typecheck

Argument of type 'Record<string, ReportAttributes> | undefined' is not assignable to parameter of type 'string | undefined'.

const goBack = () => {
Navigation.goBack(backPath);
Expand All @@ -127,16 +137,26 @@
acc[login] = accountID;
return acc;
}, {} as InvitedEmailsToAccountIDs);
const [newAccountIDsAndLogins] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: newAccountIDsAndLoginsSelector(invitedEmailsToAccountIDs)});
const [newAccountIDsAndLogins] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {
selector: newAccountIDsAndLoginsSelector(invitedEmailsToAccountIDs),
});

const inviteUsers = () => {
if (selectedOptions.length === 0) {
return;
}

if (Object.keys(invitedEmailsToAccountIDs).length === 0) {
return;
}
inviteToGroupChat(report, invitedEmailsToAccountIDs, newAccountIDsAndLogins?.newAccountIDs ?? [], newAccountIDsAndLogins?.newLogins ?? [], formatPhoneNumber);
goBack();
};

if (!shouldShowInvitePage) {
return <FullPageNotFoundView shouldShow />;
}

const getHeaderMessageText = () => {
if (sections.length > 0) {
return '';
Expand Down
52 changes: 39 additions & 13 deletions src/pages/DynamicReportParticipantsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
import usePolicy from '@hooks/usePolicy';
import useReportAttributes from '@hooks/useReportAttributes';
import useReportIsArchived from '@hooks/useReportIsArchived';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
Expand All @@ -30,16 +31,19 @@
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {ParticipantsNavigatorParamList} from '@libs/Navigation/types';
import {temporaryGetDisplayNameOrDefault} from '@libs/PersonalDetailsUtils';
import {deprecatedGetReportName} from '@libs/ReportNameUtils';
import {isPolicyAdmin} from '@libs/PolicyUtils';
import {getReportName} from '@libs/ReportNameUtils';
import {
getReportPersonalDetailsParticipants,
isAnnounceRoom,
isArchivedNonExpenseReport,
isChatRoom,
isChatThread,
isCurrentUserSubmitter,
isGroupChatAdmin,
isGroupChat as isGroupChatUtils,
isMoneyRequestReport,
isOpenExpenseReport,
isPolicyExpenseChat,
isSelfDM,
isTaskReport,
Expand Down Expand Up @@ -79,16 +83,20 @@
const tableRef = useRef<TableHandle<ReportParticipantRowData, ReportParticipantsTableColumnKey, string>>(null);
const isReportArchived = useReportIsArchived(report?.reportID);
const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report?.reportID}`);
const reportAttributes = useReportAttributes();
const isMobileSelectionModeEnabled = useMobileSelectionMode();
const [session] = useOnyx(ONYXKEYS.SESSION);
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
const currentUserAccountID = Number(session?.accountID);
const isCurrentUserAdmin = isGroupChatAdmin(report, currentUserAccountID);
const isGroupChat = isGroupChatUtils(report);
const policy = usePolicy(report.policyID);
const isCurrentUserGroupChatAdmin = isGroupChat && isCurrentUserAdmin;
const isReportSubmitterOrAdmin = isCurrentUserSubmitter(report) || isPolicyAdmin(policy) || isCurrentUserGroupChatAdmin;
const shouldShowInviteButton = isReportSubmitterOrAdmin && (isGroupChat || (isMoneyRequestReport(report) && isOpenExpenseReport(report)));

const {isOffline} = useNetwork();
const canSelectMultiple = isGroupChat && isCurrentUserAdmin && (isSmallScreenWidth ? isMobileSelectionModeEnabled : true);
const reportAttributes = useReportAttributes();

const {personalDetailsParticipants, participantsForDisplay} = getReportPersonalDetailsParticipants(report, personalDetails, reportMetadata);
const participantsForDisplayMap = participantsForDisplay.reduce<Record<number, TupleToUnion<typeof participantsForDisplay>>>((acc, participant) => {
Expand All @@ -104,8 +112,11 @@
};

const [selectedMembers, setSelectedMembers] = useFilteredSelection(personalDetailsParticipants, filterParticipants);
const shouldShowBulkActionsDropdown = isGroupChat && (isSmallScreenWidth ? canSelectMultiple : selectedMembers.length > 0);
const firstSelectedMember = selectedMembers?.at(0);
const [firstSelectedMemberDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: personalDetailsSelector(firstSelectedMember)});
const [firstSelectedMemberDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {
selector: personalDetailsSelector(firstSelectedMember),
});

// The Table stores selection as string keys, while this page tracks accountIDs as numbers.
const onRowSelectionChange = (keys: string[]) => setSelectedMembers(keys.map(Number));
Expand Down Expand Up @@ -141,7 +152,9 @@

const showRemoveMembersModal = async () => {
const {action} = await showConfirmModal({
title: translate('workspace.people.removeMembersTitle', {count: selectedMembers.length}),
title: translate('workspace.people.removeMembersTitle', {
count: selectedMembers.length,
}),
prompt: translate('workspace.people.removeMembersPrompt', {
count: selectedMembers.length,
memberName: formatPhoneNumber(firstSelectedMemberDetails?.displayName ?? ''),
Expand Down Expand Up @@ -177,7 +190,12 @@
keyForList: `${accountID}`,
accountID,
login: details?.login ?? '',
name: formatPhoneNumber(temporaryGetDisplayNameOrDefault({passedPersonalDetails: details, translate})),
name: formatPhoneNumber(
temporaryGetDisplayNameOrDefault({
passedPersonalDetails: details,
translate,
}),
),
email: formatPhoneNumber(details?.login ?? ''),
isAdmin: role === CONST.REPORT.ROLE.ADMIN,
isGroupChat,
Expand All @@ -195,7 +213,9 @@

const bulkActionsButtonOptions: Array<DropdownOption<WorkspaceMemberBulkActionType>> = [
{
text: translate('workspace.people.removeMembersTitle', {count: selectedMembers.length}),
text: translate('workspace.people.removeMembersTitle', {
count: selectedMembers.length,
}),
value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.REMOVE,
icon: icons.RemoveMembers,
onSelected: showRemoveMembersModal,
Expand All @@ -204,7 +224,9 @@

if (isAtLeastOneAdminSelected) {
bulkActionsButtonOptions.push({
text: translate('workspace.people.makeMember', {count: selectedMembers.length}),
text: translate('workspace.people.makeMember', {
count: selectedMembers.length,
}),
value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_MEMBER,
icon: icons.User,
onSelected: () => changeUserRole(CONST.REPORT.ROLE.MEMBER),
Expand All @@ -213,7 +235,9 @@

if (isAtLeastOneMemberSelected) {
bulkActionsButtonOptions.push({
text: translate('workspace.people.makeGroupAdmin', {count: selectedMembers.length}),
text: translate('workspace.people.makeGroupAdmin', {
count: selectedMembers.length,
}),
value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_ADMIN,
icon: icons.MakeAdmin,
onSelected: () => changeUserRole(CONST.REPORT.ROLE.ADMIN),
Expand Down Expand Up @@ -247,17 +271,19 @@
navigateBackToReportDetails();
}
}}
subtitle={StringUtils.lineBreaksToSpaces(deprecatedGetReportName(report, reportAttributes))}
subtitle={StringUtils.lineBreaksToSpaces(getReportName(report, reportAttributes))}

Check failure on line 274 in src/pages/DynamicReportParticipantsPage.tsx

View workflow job for this annotation

GitHub Actions / typecheck

Argument of type 'Record<string, ReportAttributes> | undefined' is not assignable to parameter of type 'string | undefined'.
/>
<View style={[styles.pl5, styles.pr5]}>

@abzokhattab abzokhattab May 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small consistency note (relates to lines 316-318, not changed in this PR): memberNotFoundMessage still keys on isGroupChat, so the empty-search hint "use the invite button above" won't show for money request reports even though the Invite button is now visible right above. In practice this is unreachable today (the search input only appears at 12+ participants per CONST.STANDARD_LIST_ITEM_LIMIT, which money request reports rarely hit), but i think it should still be consistent with the rest of the file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@M00rish can you handle it please?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

{isGroupChat && (
{shouldShowInviteButton && (
<View style={styles.w100}>
{(isSmallScreenWidth ? canSelectMultiple : selectedMembers.length > 0) ? (
{shouldShowBulkActionsDropdown ? (
<ButtonWithDropdownMenu<WorkspaceMemberBulkActionType>
variant={CONST.BUTTON_VARIANT.SUCCESS}
shouldAlwaysShowDropdownMenu
pressOnEnter
customText={translate('workspace.common.selected', {count: selectedMembers.length})}
customText={translate('workspace.common.selected', {
count: selectedMembers.length,
})}
size={CONST.BUTTON_SIZE.MEDIUM}
onPress={() => null}
isSplitButton={false}
Expand All @@ -278,7 +304,7 @@
</View>
)}
</View>
<View style={[styles.w100, styles.flex1]}>
<View style={[styles.w100, shouldShowInviteButton ? styles.mt3 : styles.mt0, styles.flex1]}>
<ReportParticipantsTable
ref={tableRef}
members={participants}
Expand Down
Loading