Skip to content

Commit 9941398

Browse files
authored
Merge pull request Expensify#87881 from software-mansion-labs/fix/newChatPage/dataPreparation/draftRestoration
Refactor draft restoration logic
2 parents 386ef04 + 46294cd commit 9941398

4 files changed

Lines changed: 516 additions & 57 deletions

File tree

Lines changed: 4 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {navigateToAndOpenReport, searchInServer, setGroupDraft} from '@libs/acti
3232
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
3333
import Log from '@libs/Log';
3434
import Navigation from '@libs/Navigation/Navigation';
35-
import {filterAndOrderOptions, filterSelectedOptions, getHeaderMessage, getUserToInviteOption, getValidOptions} from '@libs/OptionsListUtils';
35+
import {filterAndOrderOptions, filterSelectedOptions, getHeaderMessage, getValidOptions} from '@libs/OptionsListUtils';
3636
import {doesPersonalDetailMatchSearchTerm} from '@libs/OptionsListUtils/searchMatchUtils';
3737
import type {OptionWithKey} from '@libs/OptionsListUtils/types';
3838
import type {OptionData} from '@libs/ReportUtils';
@@ -44,19 +44,15 @@ import type {ReportAttributesDerivedValue} from '@src/types/onyx/DerivedValues';
4444
import type {SelectedParticipant} from '@src/types/onyx/NewGroupChatDraft';
4545
import getEmptyArray from '@src/types/utils/getEmptyArray';
4646
import KeyboardUtils from '@src/utils/keyboard';
47+
import type SelectedOption from './types';
48+
import useGroupChatDraftParticipantSync from './useGroupDraftRestore';
4749

4850
const excludedGroupEmails = new Set<string>(CONST.EXPENSIFY_EMAILS.filter((value) => value !== CONST.EMAIL.CONCIERGE));
4951

50-
type SelectedOption = ListItem &
51-
Omit<OptionData, 'reportID'> & {
52-
reportID?: string;
53-
};
54-
5552
function useOptions(reportAttributesDerived: ReportAttributesDerivedValue['reports'] | undefined) {
5653
const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState('');
5754
const [selectedOptions, setSelectedOptions] = useState<SelectedOption[]>([]);
5855
const [betas] = useOnyx(ONYXKEYS.BETAS);
59-
const [newGroupDraft] = useOnyx(ONYXKEYS.NEW_GROUP_CHAT_DRAFT);
6056
const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE);
6157
const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST);
6258
const personalData = useCurrentUserPersonalDetails();
@@ -92,6 +88,7 @@ function useOptions(reportAttributesDerived: ReportAttributesDerivedValue['repor
9288

9389
const reports = listOptions?.reports ?? [];
9490
const personalDetails = listOptions?.personalDetails ?? [];
91+
useGroupChatDraftParticipantSync(personalDetails, !isLoading, allPersonalDetails, loginList, currentUserEmail, currentUserAccountID, selectedOptions, setSelectedOptions);
9592

9693
const defaultOptions = getValidOptions(
9794
{
@@ -152,56 +149,6 @@ function useOptions(reportAttributesDerived: ReportAttributesDerivedValue['repor
152149
searchInServer(debouncedSearchTerm);
153150
}, [debouncedSearchTerm]);
154151

155-
const participants = newGroupDraft?.participants;
156-
157-
const draftSelectedOptions: OptionData[] | null =
158-
participants && personalDetails.length
159-
? participants.reduce<OptionData[]>((result, participant) => {
160-
if (participant.accountID === personalData.accountID) {
161-
return result;
162-
}
163-
const participantOption: OptionData | undefined | null =
164-
personalDetails.find((option) => option.accountID === participant.accountID) ??
165-
getUserToInviteOption({
166-
searchValue: participant?.login,
167-
personalDetails: allPersonalDetails,
168-
loginList,
169-
currentUserEmail: personalData.email ?? '',
170-
});
171-
if (participantOption) {
172-
result.push({
173-
...participantOption,
174-
isSelected: true,
175-
});
176-
}
177-
return result;
178-
}, [])
179-
: null;
180-
181-
useEffect(() => {
182-
if (!draftSelectedOptions) {
183-
return;
184-
}
185-
186-
// eslint-disable-next-line react-hooks/set-state-in-effect
187-
setSelectedOptions((prevSelectedOptions) => {
188-
if (
189-
prevSelectedOptions.length === draftSelectedOptions.length &&
190-
prevSelectedOptions.every((prevOption, index) => {
191-
const nextOption = draftSelectedOptions.at(index);
192-
if (!nextOption) {
193-
return false;
194-
}
195-
return prevOption.accountID === nextOption.accountID && prevOption.login === nextOption.login;
196-
})
197-
) {
198-
return prevSelectedOptions;
199-
}
200-
201-
return draftSelectedOptions;
202-
});
203-
}, [draftSelectedOptions, setSelectedOptions]);
204-
205152
const handleEndReached = () => {
206153
if (!hasMore || !areOptionsInitialized || !isScreenFocusedRef.current) {
207154
return;

src/pages/NewChatPage/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type {ListItem} from '@components/SelectionList/types';
2+
import type {OptionData} from '@libs/ReportUtils';
3+
4+
type SelectedOption = ListItem &
5+
Omit<OptionData, 'reportID'> & {
6+
reportID?: string;
7+
};
8+
9+
export default SelectedOption;
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import {useFocusEffect} from '@react-navigation/native';
2+
import {useCallback, useEffect, useEffectEvent, useRef} from 'react';
3+
import type {OnyxEntry} from 'react-native-onyx';
4+
import useOnyx from '@hooks/useOnyx';
5+
import {getUserToInviteOption} from '@libs/OptionsListUtils';
6+
import type {SearchOption} from '@libs/OptionsListUtils';
7+
import ONYXKEYS from '@src/ONYXKEYS';
8+
import type {Login, PersonalDetails, PersonalDetailsList} from '@src/types/onyx';
9+
import type NewGroupChatDraft from '@src/types/onyx/NewGroupChatDraft';
10+
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
11+
import type SelectedOption from './types';
12+
13+
/**
14+
* Keeps the NewChatPage's `selectedOptions` state aligned with the `NEW_GROUP_CHAT_DRAFT` Onyx draft.
15+
*
16+
* - On mount / reload, restores the draft participants into `selectedOptions` once so an
17+
* in-progress group chat survives refreshes.
18+
* - While the screen is in the background (e.g. the user navigated to NewChatConfirmPage), mirrors
19+
* participant removals made against the draft back into `selectedOptions` so the two stay
20+
* consistent when the user returns.
21+
*/
22+
function useGroupChatDraftParticipantSync(
23+
allPersonalDetailOptions: Array<SearchOption<PersonalDetails>>,
24+
areAllPersonalDetailOptionsLoaded: boolean,
25+
allPersonalDetails: OnyxEntry<PersonalDetailsList>,
26+
loginList: OnyxEntry<Login>,
27+
currentUserEmail: string,
28+
currentUserAccountID: number,
29+
selectedOptions: SelectedOption[],
30+
setSelectedOptions: (options: SelectedOption[]) => void,
31+
) {
32+
const shouldRestoreSelectedOptionsRef = useRef(true);
33+
const isScreenInBackgroundRef = useRef(false);
34+
35+
const draftParticipantsSelector = (draft: NewGroupChatDraft | undefined) => {
36+
const isSubscriptionActive = shouldRestoreSelectedOptionsRef.current || isScreenInBackgroundRef.current;
37+
return isSubscriptionActive ? draft?.participants : undefined;
38+
};
39+
40+
const [draftParticipants, draftParticipantsMetadata] = useOnyx(ONYXKEYS.NEW_GROUP_CHAT_DRAFT, {
41+
selector: draftParticipantsSelector,
42+
});
43+
44+
const restoreParticipantsFromDraft = useEffectEvent(() => {
45+
// Flip the ref first so the useOnyx selector disables the subscription
46+
shouldRestoreSelectedOptionsRef.current = false;
47+
48+
const restoredOptionsFromDraft = (draftParticipants ?? []).reduce<SelectedOption[]>((result, participant) => {
49+
if (participant.accountID === currentUserAccountID) {
50+
return result;
51+
}
52+
const option =
53+
allPersonalDetailOptions.find((personalDetail) => personalDetail.accountID === participant.accountID) ??
54+
getUserToInviteOption({
55+
searchValue: participant?.login,
56+
personalDetails: allPersonalDetails,
57+
loginList,
58+
currentUserEmail,
59+
});
60+
if (option) {
61+
result.push({...option, isSelected: true});
62+
}
63+
return result;
64+
}, []);
65+
66+
// No draft or only original creator in draft
67+
if (!restoredOptionsFromDraft.length) {
68+
return;
69+
}
70+
71+
setSelectedOptions(restoredOptionsFromDraft);
72+
});
73+
74+
// NewChatConfirmPage can only deselect participants,
75+
// so we don't need the complex logic from the restoreParticipantsFromDraft.
76+
// Simple filtering out of deselected participants is enough here
77+
const syncSelectedOptionsWithDraft = useEffectEvent(() => {
78+
const draftLogins = new Set((draftParticipants ?? []).map((participant) => participant.login));
79+
const filteredSelectionOptions = selectedOptions.filter((option) => draftLogins.has(option.login));
80+
81+
setSelectedOptions(filteredSelectionOptions);
82+
});
83+
84+
useFocusEffect(
85+
useCallback(() => {
86+
isScreenInBackgroundRef.current = false;
87+
88+
return () => {
89+
isScreenInBackgroundRef.current = true;
90+
};
91+
}, []),
92+
);
93+
94+
// Handle removing participants on other pages (e.g. NewChatConfirmPage)
95+
useEffect(() => {
96+
if (!isScreenInBackgroundRef.current) {
97+
return;
98+
}
99+
syncSelectedOptionsWithDraft();
100+
}, [draftParticipants]);
101+
102+
const areRestoreInputsReady = areAllPersonalDetailOptionsLoaded && !isLoadingOnyxValue(draftParticipantsMetadata);
103+
104+
// Handle reload with existing draft participants
105+
useEffect(() => {
106+
if (!shouldRestoreSelectedOptionsRef.current || !areRestoreInputsReady) {
107+
return;
108+
}
109+
restoreParticipantsFromDraft();
110+
}, [draftParticipants, areRestoreInputsReady]);
111+
}
112+
113+
export default useGroupChatDraftParticipantSync;

0 commit comments

Comments
 (0)