Skip to content

Commit fa8f8ca

Browse files
authored
Merge pull request Expensify#81494 from software-mansion-labs/@zfurtak/migrate-RoomInvitePage
Make `RoomInvitePage` use new `SelectionListWithSections`
2 parents 858b386 + e15ccc6 commit fa8f8ca

3 files changed

Lines changed: 91 additions & 91 deletions

File tree

src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ function BaseSelectionListWithSections<TItem extends ListItem>({
3939
ListItem,
4040
textInputOptions,
4141
initiallyFocusedItemKey,
42+
confirmButtonOptions,
4243
initialScrollIndex,
4344
onSelectRow,
4445
onDismissError,
@@ -183,13 +184,30 @@ function BaseSelectionListWithSections<TItem extends ListItem>({
183184
// Disable `Enter` shortcut if the active element is a button or checkbox
184185
const disableEnterShortcut = activeElementRole && [CONST.ROLE.BUTTON, CONST.ROLE.CHECKBOX].includes(activeElementRole as ButtonOrCheckBoxRoles);
185186

186-
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER || CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, selectFocusedItem, {
187+
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, selectFocusedItem, {
187188
captureOnInputs: true,
188189
shouldBubble: !getFocusedItem(),
189190
shouldStopPropagation,
190191
isActive: !disableKeyboardShortcuts && isScreenFocused && focusedIndex >= 0 && !disableEnterShortcut,
191192
});
192193

194+
useKeyboardShortcut(
195+
CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER,
196+
(e) => {
197+
if (confirmButtonOptions?.onConfirm) {
198+
const focusedOption = getFocusedItem();
199+
confirmButtonOptions?.onConfirm(e, focusedOption);
200+
return;
201+
}
202+
selectFocusedItem();
203+
},
204+
{
205+
captureOnInputs: true,
206+
shouldBubble: !getFocusedItem(),
207+
isActive: !disableKeyboardShortcuts && isScreenFocused && !confirmButtonOptions?.isDisabled,
208+
},
209+
);
210+
193211
const textInputKeyPress = (event: TextInputKeyPressEvent) => {
194212
const key = event.nativeEvent.key;
195213
if (key === CONST.KEYBOARD_SHORTCUTS.TAB.shortcutKey) {

src/components/SelectionList/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ type BaseSelectionListProps<TItem extends ListItem> = {
9696

9797
/** Called when the list is scrolled and the user begins dragging */
9898
onScrollBeginDrag?: () => void;
99+
100+
/** Configuration for the confirm button */
101+
confirmButtonOptions?: ConfirmButtonOptions<TItem>;
99102
};
100103

101104
/**
@@ -117,9 +120,6 @@ type SelectionListProps<TItem extends ListItem> = Partial<ChildrenProps> &
117120
/** Callback to fire when the item is long pressed */
118121
onLongPressRow?: (item: TItem) => void;
119122

120-
/** Configuration for the confirm button */
121-
confirmButtonOptions?: ConfirmButtonOptions<TItem>;
122-
123123
/** Custom header content to render instead of the default select all header */
124124
customListHeader?: React.ReactNode;
125125

src/pages/RoomInvitePage.tsx

Lines changed: 69 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {Str} from 'expensify-common';
2-
import React, {useCallback, useEffect, useMemo, useState} from 'react';
2+
import React, {useEffect, useState} from 'react';
33
import type {SectionListData} from 'react-native';
44
import {View} from 'react-native';
55
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
@@ -8,10 +8,9 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton';
88
import {usePersonalDetails} from '@components/OnyxListItemProvider';
99
import {useOptionsList} from '@components/OptionListContextProvider';
1010
import ScreenWrapper from '@components/ScreenWrapper';
11-
// eslint-disable-next-line no-restricted-imports
12-
import SelectionList from '@components/SelectionListWithSections';
13-
import InviteMemberListItem from '@components/SelectionListWithSections/InviteMemberListItem';
14-
import type {Section} from '@components/SelectionListWithSections/types';
11+
import InviteMemberListItem from '@components/SelectionList/ListItem/InviteMemberListItem';
12+
import SelectionListWithSections from '@components/SelectionList/SelectionListWithSections';
13+
import type {Section} from '@components/SelectionList/SelectionListWithSections/types';
1514
import withNavigationTransitionEnd from '@components/withNavigationTransitionEnd';
1615
import type {WithNavigationTransitionEndProps} from '@components/withNavigationTransitionEnd';
1716
import useAncestors from '@hooks/useAncestors';
@@ -78,22 +77,18 @@ function RoomInvitePage({
7877
const allPersonalDetails = usePersonalDetails();
7978

8079
// Any existing participants and Expensify emails should not be eligible for invitation
81-
const excludedUsers = useMemo(() => {
82-
const res = {
83-
...CONST.EXPENSIFY_EMAILS_OBJECT,
84-
};
85-
const visibleParticipantAccountIDs = Object.entries(report.participants ?? {})
86-
.filter(([, participant]) => participant && !isHiddenForCurrentUser(participant.notificationPreference))
87-
.map(([accountID]) => Number(accountID));
88-
for (const participant of getLoginsByAccountIDs(visibleParticipantAccountIDs)) {
89-
const smsDomain = addSMSDomainIfPhoneNumber(participant);
90-
res[smsDomain] = true;
91-
}
92-
93-
return res;
94-
}, [report.participants]);
80+
const excludedUsers: Record<string, boolean> = {
81+
...CONST.EXPENSIFY_EMAILS_OBJECT,
82+
};
83+
const visibleParticipantAccountIDs = Object.entries(report.participants ?? {})
84+
.filter(([, participant]) => participant && !isHiddenForCurrentUser(participant.notificationPreference))
85+
.map(([accountID]) => Number(accountID));
86+
for (const participant of getLoginsByAccountIDs(visibleParticipantAccountIDs)) {
87+
const smsDomain = addSMSDomainIfPhoneNumber(participant);
88+
excludedUsers[smsDomain] = true;
89+
}
9590

96-
const defaultOptions = useMemo(() => {
91+
const getDefaultOptions = () => {
9792
if (!areOptionsInitialized) {
9893
return {recentReports: [], personalDetails: [], userToInvite: null, currentUserOption: null};
9994
}
@@ -119,27 +114,19 @@ function RoomInvitePage({
119114
recentReports: [],
120115
currentUserOption: null,
121116
};
122-
}, [areOptionsInitialized, betas, excludedUsers, loginList, nvpDismissedProductTraining, options.personalDetails, selectedOptions, currentUserAccountID, currentUserEmail]);
123-
124-
const inviteOptions = useMemo(() => {
125-
if (debouncedSearchTerm.trim() === '') {
126-
return defaultOptions;
127-
}
128-
const filteredOptions = filterAndOrderOptions(defaultOptions, debouncedSearchTerm, countryCode, loginList, currentUserEmail, currentUserAccountID, allPersonalDetails, {
129-
excludeLogins: excludedUsers,
130-
});
131-
132-
return filteredOptions;
133-
}, [debouncedSearchTerm, defaultOptions, countryCode, loginList, excludedUsers, currentUserAccountID, currentUserEmail, allPersonalDetails]);
134-
135-
const sections = useMemo(() => {
136-
const sectionsArr: Sections = [];
137-
138-
const {personalDetails, userToInvite} = inviteOptions;
139-
if (!areOptionsInitialized) {
140-
return [];
141-
}
142-
117+
};
118+
const defaultOptions = getDefaultOptions();
119+
120+
const inviteOptions =
121+
debouncedSearchTerm.trim() === ''
122+
? defaultOptions
123+
: filterAndOrderOptions(defaultOptions, debouncedSearchTerm, countryCode, loginList, currentUserEmail, currentUserAccountID, allPersonalDetails, {
124+
excludeLogins: excludedUsers,
125+
});
126+
127+
const {personalDetails, userToInvite} = inviteOptions;
128+
const sections: Sections = [];
129+
if (areOptionsInitialized) {
143130
// Filter all options that is a part of the search term or in the personal details
144131
let filterSelectedOptions = selectedOptions;
145132
if (debouncedSearchTerm !== '') {
@@ -154,9 +141,10 @@ function RoomInvitePage({
154141
}
155142
const filterSelectedOptionsFormatted = filterSelectedOptions.map((selectedOption) => formatMemberForList(selectedOption));
156143

157-
sectionsArr.push({
144+
sections.push({
158145
title: undefined,
159146
data: filterSelectedOptionsFormatted,
147+
sectionIndex: 0,
160148
});
161149

162150
// Filtering out selected users from the search results
@@ -165,57 +153,50 @@ function RoomInvitePage({
165153
const personalDetailsFormatted = personalDetailsWithoutSelected.map((personalDetail) => formatMemberForList(personalDetail));
166154
const hasUnselectedUserToInvite = userToInvite && !selectedLogins.has(userToInvite.login);
167155

168-
sectionsArr.push({
156+
sections.push({
169157
title: translate('common.contacts'),
170158
data: personalDetailsFormatted,
159+
sectionIndex: 1,
171160
});
172161

173162
if (hasUnselectedUserToInvite) {
174-
sectionsArr.push({
163+
sections.push({
175164
title: undefined,
176165
data: [formatMemberForList(userToInvite)],
166+
sectionIndex: 2,
177167
});
178168
}
169+
}
179170

180-
return sectionsArr;
181-
}, [inviteOptions, areOptionsInitialized, selectedOptions, debouncedSearchTerm, translate, countryCode]);
182-
183-
const toggleOption = useCallback(
184-
(option: MemberForList) => {
185-
const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option.login);
186-
187-
let newSelectedOptions: OptionData[];
188-
if (isOptionInList) {
189-
newSelectedOptions = selectedOptions.filter((selectedOption) => selectedOption.login !== option.login);
190-
} else {
191-
newSelectedOptions = [...selectedOptions, {...option, isSelected: true}];
192-
}
171+
const toggleOption = (option: MemberForList) => {
172+
const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option.login);
193173

194-
setSelectedOptions(newSelectedOptions);
195-
},
196-
[selectedOptions],
197-
);
174+
let newSelectedOptions: OptionData[];
175+
if (isOptionInList) {
176+
newSelectedOptions = selectedOptions.filter((selectedOption) => selectedOption.login !== option.login);
177+
} else {
178+
newSelectedOptions = [...selectedOptions, {...option, isSelected: true}];
179+
}
198180

199-
const validate = useCallback(() => selectedOptions.length > 0, [selectedOptions.length]);
181+
setSelectedOptions(newSelectedOptions);
182+
};
200183

201184
// Non policy members should not be able to view the participants of a room
202185
const reportID = report?.reportID;
203-
const isPolicyEmployee = useMemo(() => isPolicyEmployeeUtil(report?.policyID, policy), [report?.policyID, policy]);
204-
const reportAction = useMemo(() => getReportAction(report?.parentReportID, report?.parentReportActionID), [report?.parentReportID, report?.parentReportActionID]);
186+
const isPolicyEmployee = isPolicyEmployeeUtil(report?.policyID, policy);
187+
const reportAction = getReportAction(report?.parentReportID, report?.parentReportActionID);
205188
const shouldParserToHTML = reportAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT;
206-
const backRoute = useMemo(() => {
207-
return reportID && (!isPolicyEmployee || isReportArchived ? ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID, backTo) : ROUTES.ROOM_MEMBERS.getRoute(reportID, backTo));
208-
}, [isPolicyEmployee, reportID, backTo, isReportArchived]);
189+
const backRoute = reportID && (!isPolicyEmployee || isReportArchived ? ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID, backTo) : ROUTES.ROOM_MEMBERS.getRoute(reportID, backTo));
209190

210191
// eslint-disable-next-line @typescript-eslint/no-deprecated
211-
const reportName = useMemo(() => getReportName(report), [report]);
192+
const reportName = getReportName(report);
212193

213194
const ancestors = useAncestors(report);
214195

215196
const inviteUsers = () => {
216197
HttpUtils.cancelPendingRequests(READ_COMMANDS.SEARCH_FOR_REPORTS);
217198

218-
if (!validate()) {
199+
if (selectedOptions.length === 0) {
219200
return;
220201
}
221202
const invitedEmailsToAccountIDs: MemberEmailsToAccountIDs = {};
@@ -242,11 +223,7 @@ function RoomInvitePage({
242223
}
243224
};
244225

245-
const goBack = useCallback(() => {
246-
Navigation.goBack(backRoute);
247-
}, [backRoute]);
248-
249-
const headerMessage = useMemo(() => {
226+
const getHeaderMessageText = () => {
250227
const searchValue = debouncedSearchTerm.trim().toLowerCase();
251228
const expensifyEmails = CONST.EXPENSIFY_EMAILS;
252229
if (!inviteOptions.userToInvite && expensifyEmails.includes(searchValue)) {
@@ -259,7 +236,7 @@ function RoomInvitePage({
259236
return translate('messages.userIsAlreadyMember', {login: searchValue, name: reportName});
260237
}
261238
return getHeaderMessage((inviteOptions.personalDetails ?? []).length !== 0, !!inviteOptions.userToInvite, debouncedSearchTerm, countryCode);
262-
}, [debouncedSearchTerm, inviteOptions.userToInvite, inviteOptions.personalDetails, excludedUsers, countryCode, translate, reportName]);
239+
};
263240

264241
useEffect(() => {
265242
updateUserSearchPhrase(debouncedSearchTerm);
@@ -271,6 +248,13 @@ function RoomInvitePage({
271248
subtitleKey = isReportArchived ? 'roomMembersPage.roomArchived' : 'roomMembersPage.notAuthorized';
272249
}
273250

251+
const textInputOptions = {
252+
value: searchTerm,
253+
label: translate('selectionList.nameEmailOrPhoneNumber'),
254+
onChangeText: setSearchTerm,
255+
headerMessage: getHeaderMessageText(),
256+
};
257+
274258
return (
275259
<ScreenWrapper
276260
shouldEnableMaxHeight
@@ -280,29 +264,27 @@ function RoomInvitePage({
280264
<FullPageNotFoundView
281265
shouldShow={isEmptyObject(report) || isReportArchived}
282266
subtitleKey={subtitleKey}
283-
onBackButtonPress={goBack}
267+
onBackButtonPress={() => Navigation.goBack(backRoute)}
284268
>
285269
<HeaderWithBackButton
286270
title={translate('workspace.invite.invitePeople')}
287271
subtitle={shouldParserToHTML ? Parser.htmlToText(reportName) : reportName}
288-
onBackButtonPress={goBack}
272+
onBackButtonPress={() => Navigation.goBack(backRoute)}
289273
/>
290-
<SelectionList
291-
canSelectMultiple
274+
<SelectionListWithSections
292275
sections={sections}
293276
ListItem={InviteMemberListItem}
294-
textInputLabel={translate('selectionList.nameEmailOrPhoneNumber')}
295-
textInputValue={searchTerm}
296-
onChangeText={(value) => {
297-
setSearchTerm(value);
298-
}}
299-
headerMessage={headerMessage}
277+
textInputOptions={textInputOptions}
300278
onSelectRow={toggleOption}
301-
onConfirm={inviteUsers}
302-
showScrollIndicator
279+
confirmButtonOptions={{
280+
onConfirm: inviteUsers,
281+
}}
303282
shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()}
304283
showLoadingPlaceholder={!areOptionsInitialized}
305284
isLoadingNewOptions={!!isSearchingForReports}
285+
disableMaintainingScrollPosition
286+
shouldShowTextInput
287+
canSelectMultiple
306288
/>
307289
<View style={[styles.flexShrink0]}>
308290
<FormAlertWithSubmitButton

0 commit comments

Comments
 (0)