Skip to content

Commit 97b8ca7

Browse files
authored
Merge pull request Expensify#82154 from software-mansion-labs/feat/improve-convierge-delayed-response
[No QA] Improve short delay before showing Concierge suggested response answer
2 parents 1ac89a3 + f960fcd commit 97b8ca7

File tree

8 files changed

+330
-27
lines changed

8 files changed

+330
-27
lines changed

src/ONYXKEYS.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,7 @@ const ONYXKEYS = {
685685
REPORT_DRAFT_COMMENT: 'reportDraftComment_',
686686
REPORT_IS_COMPOSER_FULL_SIZE: 'reportIsComposerFullSize_',
687687
REPORT_USER_IS_TYPING: 'reportUserIsTyping_',
688+
PENDING_CONCIERGE_RESPONSE: 'pendingConciergeResponse_',
688689
REPORT_USER_IS_LEAVING_ROOM: 'reportUserIsLeavingRoom_',
689690
REPORT_VIOLATIONS: 'reportViolations_',
690691
SECURITY_GROUP: 'securityGroup_',
@@ -1164,6 +1165,7 @@ type OnyxCollectionValuesMapping = {
11641165
[ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT]: string;
11651166
[ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE]: boolean;
11661167
[ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING]: OnyxTypes.ReportUserIsTyping;
1168+
[ONYXKEYS.COLLECTION.PENDING_CONCIERGE_RESPONSE]: OnyxTypes.PendingConciergeResponse;
11671169
[ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM]: boolean;
11681170
[ONYXKEYS.COLLECTION.REPORT_VIOLATIONS]: OnyxTypes.ReportViolations;
11691171
[ONYXKEYS.COLLECTION.SECURITY_GROUP]: OnyxTypes.SecurityGroup;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {useEffect} from 'react';
2+
import {applyPendingConciergeAction, discardPendingConciergeAction} from '@libs/actions/Report/SuggestedFollowup';
3+
import ONYXKEYS from '@src/ONYXKEYS';
4+
import useOnyx from './useOnyx';
5+
6+
/** If displayAfter is more than this far in the past, the response is stale (e.g. app was killed and restarted) */
7+
const STALE_THRESHOLD_MS = 10_000;
8+
9+
/**
10+
* Processes pending concierge responses stored in Onyx for a given report.
11+
* When a pending response exists, schedules the action to be moved to REPORT_ACTIONS
12+
* after the remaining delay, with automatic cleanup on unmount via useEffect.
13+
*/
14+
function usePendingConciergeResponse(reportID: string) {
15+
const [pendingResponse] = useOnyx(`${ONYXKEYS.COLLECTION.PENDING_CONCIERGE_RESPONSE}${reportID}`, {canBeMissing: true});
16+
17+
useEffect(() => {
18+
if (!pendingResponse) {
19+
return;
20+
}
21+
22+
const remaining = pendingResponse.displayAfter - Date.now();
23+
24+
// If the pending response is stale (e.g. app was killed/restarted), discard it
25+
// instead of displaying a phantom message that was never confirmed by the server.
26+
if (remaining < -STALE_THRESHOLD_MS) {
27+
discardPendingConciergeAction(reportID);
28+
return;
29+
}
30+
31+
const timer = setTimeout(
32+
() => {
33+
applyPendingConciergeAction(reportID, pendingResponse.reportAction);
34+
},
35+
Math.max(0, remaining),
36+
);
37+
38+
return () => clearTimeout(timer);
39+
}, [pendingResponse, reportID]);
40+
}
41+
42+
export default usePendingConciergeResponse;

src/libs/actions/Report/SuggestedFollowup.ts

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {Timezone} from '@src/types/onyx/PersonalDetails';
1111
import {addComment, buildOptimisticResolvedFollowups} from '.';
1212

1313
/** Delay before showing pre-generated Concierge response (in milliseconds) */
14-
const CONCIERGE_RESPONSE_DELAY_MS = 1500;
14+
const CONCIERGE_RESPONSE_DELAY_MS = 4000;
1515

1616
/**
1717
* Resolves a suggested followup by posting the selected question as a comment
@@ -89,22 +89,73 @@ function resolveSuggestedFollowup(
8989
addOptimisticConciergeActionWithDelay(reportID, optimisticConciergeAction);
9090
}
9191

92+
/**
93+
* Queues an optimistic concierge response for delayed display.
94+
* Writes action to Onyx — the usePendingConciergeResponse hook
95+
* handles the actual delay and moves the action to REPORT_ACTIONS
96+
* when the time arrives, with proper lifecycle cleanup.
97+
*/
9298
function addOptimisticConciergeActionWithDelay(reportID: string, optimisticConciergeAction: OptimisticReportAction) {
93-
// Show "Concierge is typing..." indicator
94-
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING}${reportID}`, {
95-
[CONST.ACCOUNT_ID.CONCIERGE]: true,
96-
});
99+
Onyx.update([
100+
// Store the pending response for the scheduler to process
101+
{
102+
onyxMethod: Onyx.METHOD.SET,
103+
key: `${ONYXKEYS.COLLECTION.PENDING_CONCIERGE_RESPONSE}${reportID}`,
104+
value: {
105+
reportAction: optimisticConciergeAction.reportAction,
106+
displayAfter: Date.now() + CONCIERGE_RESPONSE_DELAY_MS,
107+
},
108+
},
109+
// Show "Concierge is typing..." indicator
110+
{
111+
onyxMethod: Onyx.METHOD.MERGE,
112+
key: `${ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING}${reportID}`,
113+
value: {[CONST.ACCOUNT_ID.CONCIERGE]: true},
114+
},
115+
]);
116+
}
97117

98-
setTimeout(() => {
99-
// Clear the typing indicator
100-
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING}${reportID}`, {
101-
[CONST.ACCOUNT_ID.CONCIERGE]: false,
102-
});
118+
/**
119+
* Discards a stale pending concierge response and clears the typing indicator.
120+
* Called when the response has been pending too long (e.g. app was killed and restarted).
121+
*/
122+
function discardPendingConciergeAction(reportID: string) {
123+
Onyx.update([
124+
{
125+
onyxMethod: Onyx.METHOD.SET,
126+
key: `${ONYXKEYS.COLLECTION.PENDING_CONCIERGE_RESPONSE}${reportID}`,
127+
value: null,
128+
},
129+
{
130+
onyxMethod: Onyx.METHOD.MERGE,
131+
key: `${ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING}${reportID}`,
132+
value: {[CONST.ACCOUNT_ID.CONCIERGE]: false},
133+
},
134+
]);
135+
}
103136

104-
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {
105-
[optimisticConciergeAction.reportAction.reportActionID]: optimisticConciergeAction.reportAction,
106-
});
107-
}, CONCIERGE_RESPONSE_DELAY_MS);
137+
/**
138+
* Applies a pending concierge response by moving it to REPORT_ACTIONS
139+
* and clearing the pending state and typing indicator.
140+
*/
141+
function applyPendingConciergeAction(reportID: string, reportAction: ReportAction) {
142+
Onyx.update([
143+
{
144+
onyxMethod: Onyx.METHOD.SET,
145+
key: `${ONYXKEYS.COLLECTION.PENDING_CONCIERGE_RESPONSE}${reportID}`,
146+
value: null,
147+
},
148+
{
149+
onyxMethod: Onyx.METHOD.MERGE,
150+
key: `${ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING}${reportID}`,
151+
value: {[CONST.ACCOUNT_ID.CONCIERGE]: false},
152+
},
153+
{
154+
onyxMethod: Onyx.METHOD.MERGE,
155+
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
156+
value: {[reportAction.reportActionID]: reportAction},
157+
},
158+
]);
108159
}
109160

110-
export {resolveSuggestedFollowup, CONCIERGE_RESPONSE_DELAY_MS};
161+
export {resolveSuggestedFollowup, discardPendingConciergeAction, applyPendingConciergeAction, CONCIERGE_RESPONSE_DELAY_MS};

src/pages/inbox/report/ReportActionsView.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import useCopySelectionHelper from '@hooks/useCopySelectionHelper';
77
import useLoadReportActions from '@hooks/useLoadReportActions';
88
import useNetwork from '@hooks/useNetwork';
99
import useOnyx from '@hooks/useOnyx';
10+
import usePendingConciergeResponse from '@hooks/usePendingConciergeResponse';
1011
import usePrevious from '@hooks/usePrevious';
1112
import useReportIsArchived from '@hooks/useReportIsArchived';
1213
import useResponsiveLayout from '@hooks/useResponsiveLayout';
@@ -81,6 +82,7 @@ function ReportActionsView({
8182
isReportTransactionThread,
8283
}: ReportActionsViewProps) {
8384
useCopySelectionHelper();
85+
usePendingConciergeResponse(report.reportID);
8486
const route = useRoute<PlatformStackRouteProp<ReportsSplitNavigatorParamList, typeof SCREENS.REPORT>>();
8587
const isReportArchived = useReportIsArchived(report?.reportID);
8688
const canPerformWriteAction = useMemo(() => canUserPerformWriteAction(report, isReportArchived), [report, isReportArchived]);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type ReportAction from './ReportAction';
2+
3+
/** Pending concierge response queued for delayed display in a report */
4+
type PendingConciergeResponse = {
5+
/** The optimistic report action to add after the delay */
6+
reportAction: ReportAction;
7+
8+
/** Timestamp (ms) after which the response should be displayed */
9+
displayAfter: number;
10+
};
11+
12+
export default PendingConciergeResponse;

src/types/onyx/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ import type OnyxInputOrEntry from './OnyxInputOrEntry';
9494
import type {AnyOnyxUpdatesFromServer, OnyxUpdateEvent, OnyxUpdatesFromServer} from './OnyxUpdatesFromServer';
9595
import type {DecisionName, OriginalMessageIOU} from './OriginalMessage';
9696
import type Pages from './Pages';
97+
import type PendingConciergeResponse from './PendingConciergeResponse';
9798
import type {PendingContactAction} from './PendingContactAction';
9899
import type PersonalBankAccount from './PersonalBankAccount';
99100
import type {PersonalDetailsList, PersonalDetailsMetadata} from './PersonalDetails';
@@ -231,6 +232,7 @@ export type {
231232
OnyxUpdatesFromServer,
232233
AnyOnyxUpdatesFromServer,
233234
Pages,
235+
PendingConciergeResponse,
234236
PersonalBankAccount,
235237
PersonalDetails,
236238
PersonalDetailsList,

tests/actions/ReportTest.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4190,26 +4190,23 @@ describe('actions/Report', () => {
41904190
await waitForBatchedUpdates();
41914191

41924192
// Verify the followup-list was marked as selected
4193-
let reportActions = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}` as const);
4193+
const reportActions = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}` as const);
41944194
const updatedHtml = (reportActions?.[REPORT_ACTION_ID]?.message as Message[])?.at(0)?.html;
41954195
expect(updatedHtml).toContain('<followup-list selected>');
41964196

41974197
// Verify addComment was called (which triggers ADD_COMMENT API call)
41984198
// With pre-generated response, the API call should include the optimistic Concierge response params
41994199
TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.ADD_COMMENT, 1);
42004200

4201-
// Wait for the delayed Concierge response (1500ms delay in SuggestedFollowup.ts)
4202-
await new Promise((resolve) => {
4203-
setTimeout(resolve, CONCIERGE_RESPONSE_DELAY_MS + 100);
4204-
});
4205-
await waitForBatchedUpdates();
4201+
// Verify the pending concierge response was written to Onyx (the hook will process it)
4202+
const pendingResponse = await getOnyxValue(`${ONYXKEYS.COLLECTION.PENDING_CONCIERGE_RESPONSE}${REPORT_ID}` as const);
4203+
expect(pendingResponse).not.toBeNull();
4204+
expect(pendingResponse?.reportAction.actorAccountID).toBe(CONST.ACCOUNT_ID.CONCIERGE);
4205+
expect(pendingResponse?.displayAfter).toBeGreaterThan(Date.now() - CONCIERGE_RESPONSE_DELAY_MS);
42064206

4207-
// Verify an optimistic Concierge report action was created
4208-
reportActions = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}` as const);
4209-
const allReportActions = Object.values(reportActions ?? {});
4210-
const conciergeActions = allReportActions.filter((action) => action?.actorAccountID === CONST.ACCOUNT_ID.CONCIERGE);
4211-
// Should have 2 Concierge actions: the original one and the optimistic response
4212-
expect(conciergeActions.length).toBe(2);
4207+
// Verify the typing indicator was set
4208+
const typingStatus = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING}${REPORT_ID}` as const);
4209+
expect(typingStatus?.[CONST.ACCOUNT_ID.CONCIERGE]).toBe(true);
42134210
});
42144211
});
42154212

0 commit comments

Comments
 (0)