Skip to content

Commit 7a4e5dd

Browse files
authored
Merge pull request Expensify#88799 from neerajbachani/fix/85793-stale-dm-openreport-fallback
fix: recover stale DM access when opening profile message
2 parents 8cd7332 + 0c7bf5f commit 7a4e5dd

5 files changed

Lines changed: 323 additions & 49 deletions

File tree

src/components/PromotedActionsBar.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,18 +76,22 @@ const PromotedActions = {
7676
icon: 'CommentBubbles',
7777
translationKey: 'common.message',
7878
onSelected: () => {
79-
if (reportID) {
79+
if (reportID && accountID === currentUserAccountID) {
8080
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID));
8181
return;
8282
}
8383

84-
// The accountID might be optimistic, so we should use the login if we have it
8584
if (login) {
86-
navigateToAndOpenReport([login], personalDetails, currentUserAccountID, introSelected, isSelfTourViewed, betas, false);
85+
navigateToAndOpenReport([login], personalDetails, currentUserAccountID, introSelected, isSelfTourViewed, betas, false, true);
8786
return;
8887
}
8988
if (accountID) {
90-
navigateToAndOpenReportWithAccountIDs([accountID], currentUserAccountID, introSelected, isSelfTourViewed, betas, personalDetails);
89+
navigateToAndOpenReportWithAccountIDs([accountID], currentUserAccountID, introSelected, isSelfTourViewed, betas, personalDetails, true);
90+
return;
91+
}
92+
93+
if (reportID) {
94+
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID));
9195
}
9296
},
9397
}),

src/libs/actions/Report/index.ts

Lines changed: 160 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ import {
167167
isOpenExpenseReport,
168168
isProcessingReport,
169169
isReportManuallyReimbursed,
170+
isReportNotFound,
170171
isSelfDM,
171172
isValidReportIDFromPath,
172173
prepareOnboardingOnyxData,
@@ -400,6 +401,53 @@ type AddAttachmentWithCommentParams = {
400401
const addNewMessageWithText = new Set<string>([WRITE_COMMANDS.ADD_COMMENT, WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT]);
401402
// map of reportID to all reportActions for that report
402403
const allReportActions: OnyxCollection<ReportActions> = {};
404+
const STALE_DM_RECOVERY_TARGET_TTL_MS = 30000;
405+
const staleDMRecoveryTargetBySourceReportID: Record<string, string> = {};
406+
const staleDMRecoverySourceByTargetReportID: Record<string, string> = {};
407+
const staleDMRecoveryCleanupTimersBySourceReportID: Record<string, ReturnType<typeof setTimeout>> = {};
408+
409+
function clearStaleDMRecoveryTargetBySourceReportID(sourceReportID: string) {
410+
const targetReportID = staleDMRecoveryTargetBySourceReportID[sourceReportID];
411+
if (!targetReportID) {
412+
return;
413+
}
414+
415+
delete staleDMRecoveryTargetBySourceReportID[sourceReportID];
416+
delete staleDMRecoverySourceByTargetReportID[targetReportID];
417+
418+
const timeoutID = staleDMRecoveryCleanupTimersBySourceReportID[sourceReportID];
419+
if (timeoutID) {
420+
clearTimeout(timeoutID);
421+
}
422+
delete staleDMRecoveryCleanupTimersBySourceReportID[sourceReportID];
423+
}
424+
425+
function setStaleDMRecoveryTarget(sourceReportID: string, targetReportID: string) {
426+
clearStaleDMRecoveryTargetBySourceReportID(sourceReportID);
427+
const existingSourceReportID = staleDMRecoverySourceByTargetReportID[targetReportID];
428+
if (existingSourceReportID) {
429+
clearStaleDMRecoveryTargetBySourceReportID(existingSourceReportID);
430+
}
431+
432+
staleDMRecoveryTargetBySourceReportID[sourceReportID] = targetReportID;
433+
staleDMRecoverySourceByTargetReportID[targetReportID] = sourceReportID;
434+
staleDMRecoveryCleanupTimersBySourceReportID[sourceReportID] = setTimeout(() => {
435+
clearStaleDMRecoveryTargetBySourceReportID(sourceReportID);
436+
}, STALE_DM_RECOVERY_TARGET_TTL_MS);
437+
}
438+
439+
function getStaleDMRecoveryTarget(reportID: string) {
440+
return staleDMRecoveryTargetBySourceReportID[reportID];
441+
}
442+
443+
function clearStaleDMRecoveryTargetByTargetReportID(targetReportID: string) {
444+
const sourceReportID = staleDMRecoverySourceByTargetReportID[targetReportID];
445+
if (!sourceReportID) {
446+
return;
447+
}
448+
449+
clearStaleDMRecoveryTargetBySourceReportID(sourceReportID);
450+
}
403451

404452
Onyx.connect({
405453
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
@@ -762,7 +810,18 @@ function addActions({
762810
if (!report?.reportID) {
763811
return;
764812
}
765-
const reportID = report.reportID;
813+
const sourceReportID = report.reportID;
814+
const reportID = getStaleDMRecoveryTarget(sourceReportID) ?? sourceReportID;
815+
const reportForAction = reportID === sourceReportID ? report : (allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? report);
816+
817+
let resolvedNotifyReportID: AddActionsParams['notifyReportID'];
818+
if (typeof notifyReportID === 'string') {
819+
resolvedNotifyReportID = getStaleDMRecoveryTarget(notifyReportID) ?? notifyReportID;
820+
} else if (Array.isArray(notifyReportID)) {
821+
resolvedNotifyReportID = notifyReportID.map((id) => getStaleDMRecoveryTarget(id) ?? id);
822+
} else {
823+
resolvedNotifyReportID = notifyReportID;
824+
}
766825
let reportCommentText = '';
767826
let reportCommentAction: OptimisticAddCommentReportAction | undefined;
768827
let attachmentAction: OptimisticAddCommentReportAction | undefined;
@@ -832,10 +891,10 @@ function addActions({
832891
lastActionType: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT,
833892
};
834893

835-
const shouldUpdateNotificationPreference = !isEmptyObject(report) && isHiddenForCurrentUser(report);
894+
const shouldUpdateNotificationPreference = !isEmptyObject(reportForAction) && isHiddenForCurrentUser(reportForAction);
836895
if (shouldUpdateNotificationPreference) {
837896
optimisticReport.participants = {
838-
[currentUserAccountID]: {notificationPreference: getDefaultNotificationPreferenceForReport(report)},
897+
[currentUserAccountID]: {notificationPreference: getDefaultNotificationPreferenceForReport(reportForAction)},
839898
};
840899
}
841900

@@ -870,7 +929,7 @@ function addActions({
870929
idempotencyKey: Str.guid(),
871930
};
872931

873-
const isConciergeChat = isConciergeChatReport(report);
932+
const isConciergeChat = isConciergeChatReport(reportForAction);
874933
if (reportIDDeeplinkedFromOldDot === reportID && isConciergeChat) {
875934
parameters.isOldDotConciergeChat = true;
876935
}
@@ -879,7 +938,7 @@ function addActions({
879938
parameters.attachmentID = attachmentID;
880939
}
881940

882-
if (isInSidePanel && (isConciergeChat || isAdminRoom(report))) {
941+
if (isInSidePanel && (isConciergeChat || isAdminRoom(reportForAction))) {
883942
const pageHTML = capturePageHTML();
884943
if (pageHTML) {
885944
parameters.pageHTML = pageHTML;
@@ -1000,7 +1059,7 @@ function addActions({
10001059
successData,
10011060
failureData,
10021061
});
1003-
notifyNewAction(notifyReportID, lastAction, lastAction?.actorAccountID === currentUserAccountID);
1062+
notifyNewAction(resolvedNotifyReportID, lastAction, lastAction?.actorAccountID === currentUserAccountID);
10041063
}
10051064

10061065
/** Add an attachment with an optional comment to a report */
@@ -1397,7 +1456,6 @@ function openReport(params: OpenReportActionParams) {
13971456
const participantLoginList = participants.map((p) => p.login).filter((login) => !!login);
13981457
// TODO: allPersonalDetails fallback should be removed in follow-up PRs https://github.com/Expensify/App/issues/73656
13991458
const participantAccountIDList = participants.map((p) => p.accountID).filter((id): id is number => id !== undefined);
1400-
14011459
const optimisticReport = reportActionsExist(reportID)
14021460
? {}
14031461
: {
@@ -2101,17 +2159,10 @@ function createTransactionThreadReport(
21012159
*/
21022160
function navigateToReport(reportID: string | undefined, shouldDismissModal = true) {
21032161
if (shouldDismissModal) {
2104-
Navigation.dismissModal({
2105-
afterTransition: () => {
2106-
if (!reportID) {
2107-
return;
2108-
}
2109-
2110-
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID));
2111-
},
2112-
});
2113-
} else if (reportID) {
2114-
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID));
2162+
Navigation.dismissModal();
2163+
}
2164+
if (!reportID) {
2165+
return;
21152166
}
21162167
// In some cases when RHP modal gets hidden and then we navigate to report Composer focus breaks, wrapping navigation in setTimeout fixes this
21172168
setTimeout(() => {
@@ -2134,32 +2185,68 @@ function navigateToAndOpenReport(
21342185
isSelfTourViewed: boolean | undefined,
21352186
betas: OnyxEntry<Beta[]>,
21362187
shouldDismissModal = true,
2188+
shouldRevalidateExistingChat = false,
21372189
) {
2138-
let newChat: OptimisticChatReport | undefined;
21392190
const participantAccountIDs = PersonalDetailsUtils.getAccountIDsByLogins(userLogins);
21402191
const chat = getChatByParticipants([...participantAccountIDs, currentUserAccountID]);
2141-
2142-
if (isEmptyObject(chat)) {
2143-
newChat = buildOptimisticChatReport({
2192+
const createAndOpenNewOptimisticChat = (sourceCachedReportID?: string) => {
2193+
const fallbackChat = buildOptimisticChatReport({
21442194
participantList: [...participantAccountIDs, currentUserAccountID],
21452195
notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN,
21462196
currentUserAccountID,
21472197
});
2148-
// We want to pass newChat here because if anything is passed in that param (even an existing chat), we will try to create a chat on the server
2198+
if (sourceCachedReportID) {
2199+
setStaleDMRecoveryTarget(sourceCachedReportID, fallbackChat.reportID);
2200+
}
2201+
2202+
// We pass newReportObject to force chat creation on the server.
21492203
openReport({
2150-
reportID: newChat?.reportID,
2204+
reportID: fallbackChat.reportID,
21512205
introSelected,
21522206
reportActionID: '',
21532207
participants: buildParticipantInfoFromLogins(userLogins),
21542208
personalDetails,
2155-
newReportObject: newChat,
2209+
newReportObject: fallbackChat,
21562210
isSelfTourViewed,
21572211
betas,
21582212
});
2213+
2214+
navigateToReport(fallbackChat.reportID, shouldDismissModal);
2215+
};
2216+
2217+
if (isEmptyObject(chat) || isReportNotFound(chat)) {
2218+
createAndOpenNewOptimisticChat(chat?.reportID);
2219+
return;
21592220
}
2160-
const report = isEmptyObject(chat) ? newChat : chat;
21612221

2162-
navigateToReport(report?.reportID, shouldDismissModal);
2222+
if (!shouldRevalidateExistingChat) {
2223+
navigateToReport(chat.reportID, shouldDismissModal);
2224+
return;
2225+
}
2226+
2227+
let hasAttemptedFallback = false;
2228+
const reportConnection = Onyx.connectWithoutView({
2229+
key: `${ONYXKEYS.COLLECTION.REPORT}${chat.reportID}`,
2230+
callback: (updatedReport) => {
2231+
const notFoundError = updatedReport?.errorFields?.notFound;
2232+
if (notFoundError === null) {
2233+
Onyx.disconnect(reportConnection);
2234+
return;
2235+
}
2236+
2237+
if (!notFoundError || hasAttemptedFallback) {
2238+
return;
2239+
}
2240+
2241+
hasAttemptedFallback = true;
2242+
Onyx.disconnect(reportConnection);
2243+
createAndOpenNewOptimisticChat(chat.reportID);
2244+
},
2245+
});
2246+
2247+
// Re-open existing chats to re-validate server-side access and refresh stale local state.
2248+
openReport({reportID: chat.reportID, introSelected, isSelfTourViewed, betas});
2249+
navigateToReport(chat.reportID, shouldDismissModal);
21632250
}
21642251

21652252
function navigateToAndCreateGroupChat(
@@ -2196,37 +2283,73 @@ function navigateToAndOpenReportWithAccountIDs(
21962283
betas: OnyxEntry<Beta[]>,
21972284
// TODO: personalDetails should be a required field in follow-up PRs https://github.com/Expensify/App/issues/73656
21982285
personalDetails?: OnyxEntry<PersonalDetailsList>,
2286+
shouldRevalidateExistingChat = false,
21992287
) {
2200-
let newChat: OptimisticChatReport | undefined;
22012288
const participants = participantAccountIDs.map((accountID): ParticipantInfo => {
22022289
return {
22032290
login: '',
22042291
accountID,
22052292
};
22062293
});
2207-
22082294
const chat = getChatByParticipants([...participantAccountIDs, currentUserAccountID]);
2209-
if (!chat) {
2210-
newChat = buildOptimisticChatReport({
2295+
const createAndOpenNewOptimisticChat = (sourceCachedReportID?: string) => {
2296+
const fallbackChat = buildOptimisticChatReport({
22112297
participantList: [...participantAccountIDs, currentUserAccountID],
22122298
currentUserAccountID,
22132299
});
2214-
// We want to pass newChat here because if anything is passed in that param (even an existing chat), we will try to create a chat on the server
2300+
if (sourceCachedReportID) {
2301+
setStaleDMRecoveryTarget(sourceCachedReportID, fallbackChat.reportID);
2302+
}
2303+
2304+
// We pass newReportObject to force chat creation on the server.
22152305
openReport({
2216-
reportID: newChat?.reportID,
2306+
reportID: fallbackChat.reportID,
22172307
introSelected,
22182308
isSelfTourViewed,
2219-
newReportObject: newChat,
2309+
newReportObject: fallbackChat,
22202310
parentReportActionID: '0',
22212311
participants,
22222312
// TODO: allPersonalDetails fallback should be removed in follow-up PRs https://github.com/Expensify/App/issues/73656
22232313
personalDetails: personalDetails ?? allPersonalDetails,
22242314
betas,
22252315
});
2316+
2317+
navigateToReport(fallbackChat.reportID, false);
2318+
};
2319+
2320+
if (!chat || isReportNotFound(chat)) {
2321+
createAndOpenNewOptimisticChat(chat?.reportID);
2322+
return;
22262323
}
2227-
const report = chat ?? newChat;
22282324

2229-
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report?.reportID));
2325+
if (!shouldRevalidateExistingChat) {
2326+
navigateToReport(chat.reportID, false);
2327+
return;
2328+
}
2329+
2330+
let hasAttemptedFallback = false;
2331+
const reportConnection = Onyx.connectWithoutView({
2332+
key: `${ONYXKEYS.COLLECTION.REPORT}${chat.reportID}`,
2333+
callback: (updatedReport) => {
2334+
const notFoundError = updatedReport?.errorFields?.notFound;
2335+
if (notFoundError === null) {
2336+
Onyx.disconnect(reportConnection);
2337+
return;
2338+
}
2339+
2340+
if (!notFoundError || hasAttemptedFallback) {
2341+
return;
2342+
}
2343+
2344+
hasAttemptedFallback = true;
2345+
Onyx.disconnect(reportConnection);
2346+
createAndOpenNewOptimisticChat(chat.reportID);
2347+
},
2348+
});
2349+
2350+
// Re-open existing chats to re-validate server-side access and refresh stale local state.
2351+
openReport({reportID: chat.reportID, introSelected, isSelfTourViewed, betas});
2352+
navigateToReport(chat.reportID, false);
22302353
}
22312354

22322355
/**
@@ -7686,6 +7809,7 @@ export {
76867809
setNewRoomFormLoading,
76877810
clearPolicyRoomNameErrors,
76887811
clearPrivateNotesError,
7812+
clearStaleDMRecoveryTargetByTargetReportID,
76897813
clearReportFieldKeyErrors,
76907814
completeOnboarding,
76917815
extractRHPVariantFromResponse,

src/pages/inbox/ReportFetchHandler.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,15 @@ import {getFilteredReportActionsForReportView, getIOUActionForReportID, getOneTr
1919
import {isChatThread, isHiddenForCurrentUser, isOneTransactionThread, isPolicyExpenseChat, isReportTransactionThread, isTaskReport, isValidReportIDFromPath} from '@libs/ReportUtils';
2020
import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList} from '@navigation/types';
2121
import {setShouldShowComposeInput} from '@userActions/Composer';
22-
import {createTransactionThreadReport, openReport, readNewestAction, subscribeToReportLeavingEvents, unsubscribeFromLeavingRoomReportChannel, updateLastVisitTime} from '@userActions/Report';
22+
import {
23+
clearStaleDMRecoveryTargetByTargetReportID,
24+
createTransactionThreadReport,
25+
openReport,
26+
readNewestAction,
27+
subscribeToReportLeavingEvents,
28+
unsubscribeFromLeavingRoomReportChannel,
29+
updateLastVisitTime,
30+
} from '@userActions/Report';
2331
import CONST from '@src/CONST';
2432
import ONYXKEYS from '@src/ONYXKEYS';
2533
import SCREENS from '@src/SCREENS';
@@ -268,13 +276,14 @@ function ReportFetchHandler() {
268276
}, [isFocused, prevIsFocused]);
269277

270278
useEffect(() => {
271-
if (!isValidReportIDFromPath(reportIDFromRoute)) {
279+
if (!reportIDFromRoute || !isValidReportIDFromPath(reportIDFromRoute)) {
272280
return;
273281
}
274282
// Ensures the optimistic report is created successfully
275283
if (reportIDFromRoute !== report?.reportID || report?.pendingFields?.createChat) {
276284
return;
277285
}
286+
clearStaleDMRecoveryTargetByTargetReportID(reportIDFromRoute);
278287
// Ensures subscription event succeeds when the report/workspace room is created optimistically.
279288
// Check if the optimistic `OpenReport` or `AddWorkspaceRoom` has succeeded by confirming
280289
// any `pendingFields.createChat` or `pendingFields.addWorkspaceRoom` fields are set to null.

0 commit comments

Comments
 (0)