Skip to content

fix: persist selection in room members page #59648

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
May 4, 2025
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
561def7
fix: persist selection in room members page
daledah Apr 4, 2025
fb5f4fd
Merge branch 'main' into fix/59426
daledah Apr 8, 2025
6b821e5
fix: preserve selection in other pages
daledah Apr 8, 2025
944bba8
fix: lint
daledah Apr 8, 2025
cf6136e
Merge branch 'main' into fix/59426
daledah Apr 9, 2025
4190dfa
fix: persist selection on report fields page, multi level tags page
daledah Apr 9, 2025
fe709dd
fix: check for exist tags
daledah Apr 10, 2025
c3d8f0e
Merge branch 'main' into fix/59426
daledah Apr 11, 2025
f1880f5
Merge branch 'main' into fix/59426
daledah Apr 16, 2025
598d174
fix: create hook to unify selection persist logic
daledah Apr 16, 2025
0096a98
fix: lint
daledah Apr 16, 2025
ebbf641
Merge branch 'main' into fix/59426
daledah Apr 17, 2025
9a75daf
refactor: use array as state in usePersistSelection
daledah Apr 17, 2025
568e5f1
Merge branch 'main' into fix/59426
daledah Apr 21, 2025
af401ee
Merge branch 'main' into fix/59426
daledah Apr 22, 2025
a1f1b20
fix: simplify usePersistSelection
daledah Apr 22, 2025
1ad720c
fix: lint
daledah Apr 22, 2025
8e43b1c
Merge branch 'main' into fix/59426
daledah Apr 24, 2025
5c5fa04
fix: update suggestions
daledah Apr 24, 2025
3f95d47
Merge branch 'main' into fix/59426
daledah Apr 24, 2025
2a7c9bd
Merge branch 'main' into fix/59426
daledah Apr 25, 2025
6bc0bf1
Merge branch 'main' into fix/59426
daledah Apr 29, 2025
b1fc61b
fix: apply suggestions
daledah Apr 29, 2025
3d42bf2
Merge branch 'main' into fix/59426
daledah Apr 30, 2025
9b40a16
fix: app crash and incorrect selected items
daledah Apr 30, 2025
2bbd6be
Merge branch 'main' into fix/59426
daledah May 1, 2025
8f0452d
fix: filter pending action for toggle all distance rates
daledah May 1, 2025
7f1429c
Merge branch 'main' into fix/59426
daledah May 2, 2025
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
19 changes: 19 additions & 0 deletions src/hooks/useFilteredSelection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {useEffect, useState} from 'react';

/**
* Custom hook to manage a selection of keys from a given set of options.
* It filters the selected keys based on a provided filter function whenever the options or the filter change.
*
* @param options - Option data
* @param filter - Filter function
* @returns A tuple containing the array of selected keys and a function to update the selected keys.
*/
function useFilteredSelection<TKey extends string | number, TValue>(options: Record<TKey, TValue> | undefined, filter: (option: TValue | undefined) => boolean) {
const [selectedOptions, setSelectedOptions] = useState<TKey[]>([]);

useEffect(() => setSelectedOptions((prevOptions) => prevOptions.filter((key) => filter(options?.[key]))), [options, filter]);

return [selectedOptions, setSelectedOptions] as const;
}

export default useFilteredSelection;
19 changes: 17 additions & 2 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2760,11 +2760,11 @@ function getParticipantsAccountIDsForDisplay(
return participantsIds.filter((accountID) => isNumber(accountID));
}

function getParticipantsList(report: Report, personalDetails: OnyxEntry<PersonalDetailsList>, isRoomMembersList = false): number[] {
function getParticipantsList(report: Report, personalDetails: OnyxEntry<PersonalDetailsList>, isRoomMembersList = false, reportMetadata: OnyxEntry<ReportMetadata> = undefined): number[] {
const isReportGroupChat = isGroupChat(report);
const isReportIOU = isIOUReport(report);
const shouldExcludeHiddenParticipants = !isReportGroupChat && !isReportIOU;
const chatParticipants = getParticipantsAccountIDsForDisplay(report, isRoomMembersList || shouldExcludeHiddenParticipants);
const chatParticipants = getParticipantsAccountIDsForDisplay(report, isRoomMembersList || shouldExcludeHiddenParticipants, false, false, reportMetadata);

return chatParticipants.filter((accountID) => {
const details = personalDetails?.[accountID];
Expand Down Expand Up @@ -10436,6 +10436,20 @@ function getChatListItemReportName(action: ReportAction & {reportName?: string},
return action?.reportName ?? '';
}

function getReportPersonalDetailsParticipants(report: Report, personalDetailsParam: OnyxEntry<PersonalDetailsList>, reportMetadata: OnyxEntry<ReportMetadata>, isRoomMembersList = false) {
const chatParticipants = getParticipantsList(report, personalDetailsParam, isRoomMembersList, reportMetadata);
return {
chatParticipants,
personalDetailsParticipants: chatParticipants.reduce<Record<number, PersonalDetails>>((acc, accountID) => {
const details = personalDetailsParam?.[accountID];
if (details) {
acc[accountID] = details;
}
return acc;
}, {}),
};
}

export {
addDomainToShortMention,
completeShortMention,
Expand Down Expand Up @@ -10805,6 +10819,7 @@ export {
populateOptimisticReportFormula,
getOutstandingReportsForUser,
isReportOutstanding,
getReportPersonalDetailsParticipants,
isAllowedToSubmitDraftExpenseReport,
};

Expand Down
33 changes: 23 additions & 10 deletions src/pages/ReportParticipantsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import TableListItem from '@components/SelectionList/TableListItem';
import type {ListItem, SelectionListHandle} from '@components/SelectionList/types';
import SelectionListWithModal from '@components/SelectionListWithModal';
import Text from '@components/Text';
import useFilteredSelection from '@hooks/useFilteredSelection';
import useLocalize from '@hooks/useLocalize';
import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
import useNetwork from '@hooks/useNetwork';
Expand All @@ -33,8 +34,8 @@ import type {ParticipantsNavigatorParamList} from '@libs/Navigation/types';
import {isSearchStringMatchUserDetails} from '@libs/OptionsListUtils';
import {getDisplayNameOrDefault, getPersonalDetailsByIDs} from '@libs/PersonalDetailsUtils';
import {
getParticipantsList,
getReportName,
getReportPersonalDetailsParticipants,
isArchivedNonExpenseReport,
isChatRoom,
isChatThread,
Expand All @@ -50,6 +51,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type {PersonalDetails} from '@src/types/onyx';
import type {WithReportOrNotFoundProps} from './home/report/withReportOrNotFound';
import withReportOrNotFound from './home/report/withReportOrNotFound';

Expand All @@ -58,7 +60,6 @@ type MemberOption = Omit<ListItem, 'accountID'> & {accountID: number};
type ReportParticipantsPageProps = WithReportOrNotFoundProps & PlatformStackScreenProps<ParticipantsNavigatorParamList, typeof SCREENS.REPORT_PARTICIPANTS.ROOT>;
function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) {
const backTo = route.params.backTo;
const [selectedMembers, setSelectedMembers] = useState<number[]>([]);
const [removeMembersConfirmModalVisible, setRemoveMembersConfirmModalVisible] = useState(false);
const {translate, formatPhoneNumber} = useLocalize();
const styles = useThemeStyles();
Expand All @@ -84,14 +85,26 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) {
const canSelectMultiple = isGroupChat && isCurrentUserAdmin && (isSmallScreenWidth ? selectionMode?.isEnabled : true);
const [searchValue, setSearchValue] = useState('');

useEffect(() => {
if (isFocused) {
return;
}
setSelectedMembers([]);
}, [isFocused]);
const {chatParticipants, personalDetailsParticipants} = useMemo(
() => getReportPersonalDetailsParticipants(report, personalDetails, reportMetadata),
[report, personalDetails, reportMetadata],
);

const filterParticipants = useCallback(
(participant?: PersonalDetails) => {
if (!participant) {
return false;
}
const isInParticipants = chatParticipants.includes(participant.accountID);
const pendingChatMember = reportMetadata?.pendingChatMembers?.find((member) => member.accountID === participant.accountID.toString());

const isPendingDelete = pendingChatMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
return isInParticipants && !isPendingDelete;
},
[chatParticipants, reportMetadata?.pendingChatMembers],
);

const chatParticipants = getParticipantsList(report, personalDetails);
const [selectedMembers, setSelectedMembers] = useFilteredSelection(personalDetailsParticipants, filterParticipants);

const pendingChatMembers = reportMetadata?.pendingChatMembers;
const reportParticipants = report?.participants;
Expand Down Expand Up @@ -240,7 +253,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) {
updateGroupChatMemberRoles(report.reportID, accountIDsToUpdate, role);
setSelectedMembers([]);
},
[report, selectedMembers],
[report, selectedMembers, setSelectedMembers],
);

/**
Expand Down
63 changes: 42 additions & 21 deletions src/pages/RoomMembersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import SelectionListWithModal from '@components/SelectionListWithModal';
import Text from '@components/Text';
import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
import useFilteredSelection from '@hooks/useFilteredSelection';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
Expand All @@ -32,13 +33,14 @@ import type {RoomMembersNavigatorParamList} from '@libs/Navigation/types';
import {isPersonalDetailsReady, isSearchStringMatchUserDetails} from '@libs/OptionsListUtils';
import {getDisplayNameOrDefault, getPersonalDetailsByIDs} from '@libs/PersonalDetailsUtils';
import {isPolicyEmployee as isPolicyEmployeeUtils, isUserPolicyAdmin} from '@libs/PolicyUtils';
import {getParticipantsList, getReportName, isChatThread, isDefaultRoom, isPolicyExpenseChat as isPolicyExpenseChatUtils, isUserCreatedPolicyRoom} from '@libs/ReportUtils';
import {getReportName, getReportPersonalDetailsParticipants, isChatThread, isDefaultRoom, isPolicyExpenseChat as isPolicyExpenseChatUtils, isUserCreatedPolicyRoom} from '@libs/ReportUtils';
import StringUtils from '@libs/StringUtils';
import {clearAddRoomMemberError, openRoomMembersPage, removeFromRoom} from '@userActions/Report';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type {PersonalDetails} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {WithReportOrNotFoundProps} from './home/report/withReportOrNotFound';
import withReportOrNotFound from './home/report/withReportOrNotFound';
Expand All @@ -48,36 +50,51 @@ type RoomMembersPageProps = WithReportOrNotFoundProps & WithCurrentUserPersonalD
function RoomMembersPage({report, policies}: RoomMembersPageProps) {
const route = useRoute<PlatformStackRouteProp<RoomMembersNavigatorParamList, typeof SCREENS.ROOM_MEMBERS.ROOT>>();
const styles = useThemeStyles();
const [session] = useOnyx(ONYXKEYS.SESSION);
const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report?.reportID}`);
const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false});
const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report?.reportID}`, {canBeMissing: true});
const currentUserAccountID = Number(session?.accountID);
const {formatPhoneNumber, translate} = useLocalize();
const [selectedMembers, setSelectedMembers] = useState<number[]>([]);
const [removeMembersConfirmModalVisible, setRemoveMembersConfirmModalVisible] = useState(false);
const [userSearchPhrase] = useOnyx(ONYXKEYS.ROOM_MEMBERS_USER_SEARCH_PHRASE);
const [userSearchPhrase] = useOnyx(ONYXKEYS.ROOM_MEMBERS_USER_SEARCH_PHRASE, {canBeMissing: true});
const [searchValue, setSearchValue] = useState('');
const [didLoadRoomMembers, setDidLoadRoomMembers] = useState(false);
const personalDetails = usePersonalDetails();
const policy = useMemo(() => policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`], [policies, report?.policyID]);
const isPolicyExpenseChat = useMemo(() => isPolicyExpenseChatUtils(report), [report]);
const backTo = route.params.backTo;

const {chatParticipants: participants, personalDetailsParticipants} = useMemo(
() => getReportPersonalDetailsParticipants(report, personalDetails, reportMetadata, true),
[report, personalDetails, reportMetadata],
);

const shouldIncludeMember = useCallback(
(participant?: PersonalDetails) => {
if (!participant) {
return false;
}
const isInParticipants = participants.includes(participant.accountID);
const pendingChatMember = reportMetadata?.pendingChatMembers?.find((member) => member.accountID === participant.accountID.toString());

const isPendingDelete = pendingChatMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;

// Keep the member only if they're still in the room and not pending removal
return isInParticipants && !isPendingDelete;
},
[participants, reportMetadata?.pendingChatMembers],
);

const [selectedMembers, setSelectedMembers] = useFilteredSelection(personalDetailsParticipants, shouldIncludeMember);

const isFocusedScreen = useIsFocused();
const {isOffline} = useNetwork();

// We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to use the selection mode only on small screens
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout();
const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE);
const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE, {canBeMissing: true});
const canSelectMultiple = isSmallScreenWidth ? selectionMode?.isEnabled : true;

useEffect(() => {
if (isFocusedScreen) {
return;
}
setSelectedMembers([]);
}, [isFocusedScreen]);

/**
* Get members for the current room
*/
Expand Down Expand Up @@ -125,16 +142,22 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) {
/**
* Add user from the selectedMembers list
*/
const addUser = useCallback((accountID: number) => {
setSelectedMembers((prevSelected) => [...prevSelected, accountID]);
}, []);
const addUser = useCallback(
(accountID: number) => {
setSelectedMembers((prevSelected) => [...prevSelected, accountID]);
},
[setSelectedMembers],
);

/**
* Remove user from the selectedEmployees list
*/
const removeUser = useCallback((accountID: number) => {
setSelectedMembers((prevSelected) => prevSelected.filter((selected) => selected !== accountID));
}, []);
const removeUser = useCallback(
(accountID: number) => {
setSelectedMembers((prevSelected) => prevSelected.filter((id) => id !== accountID));
},
[setSelectedMembers],
);

/** Toggle user from the selectedMembers list */
const toggleUser = useCallback(
Expand Down Expand Up @@ -171,8 +194,6 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) {
}
};

const participants = useMemo(() => getParticipantsList(report, personalDetails, true), [report, personalDetails]);

/** Include the search bar when there are 8 or more active members in the selection list */
const shouldShowTextInput = useMemo(() => {
// Get the active chat members by filtering out the pending members with delete action
Expand Down
Loading
Loading