Skip to content

Commit ea14b5b

Browse files
authored
Merge pull request #58476 from callstack-internal/perf/create-report-attributes-derived-value
perf: create reportAttributes derived value
2 parents 31543a6 + c1802c2 commit ea14b5b

12 files changed

+188
-63
lines changed

Mobile-Expensify

src/ONYXKEYS.ts

+2
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,7 @@ const ONYXKEYS = {
790790
},
791791
DERIVED: {
792792
CONCIERGE_CHAT_REPORT_ID: 'conciergeChatReportID',
793+
REPORT_ATTRIBUTES: 'reportAttributes',
793794
},
794795
} as const;
795796

@@ -1124,6 +1125,7 @@ type OnyxValuesMapping = {
11241125

11251126
type OnyxDerivedValuesMapping = {
11261127
[ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID]: string | undefined;
1128+
[ONYXKEYS.DERIVED.REPORT_ATTRIBUTES]: Record<string, OnyxTypes.ReportAttributes>;
11271129
};
11281130

11291131
type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping & OnyxDerivedValuesMapping;

src/libs/ReportUtils.ts

+27-25
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import type {
4343
PolicyReportField,
4444
Report,
4545
ReportAction,
46+
ReportAttributes,
4647
ReportMetadata,
4748
ReportNameValuePairs,
4849
ReportViolationName,
@@ -1035,6 +1036,17 @@ Onyx.connect({
10351036
callback: (value) => (activePolicyID = value),
10361037
});
10371038

1039+
let reportAttributes: OnyxEntry<Record<string, ReportAttributes>>;
1040+
Onyx.connect({
1041+
key: ONYXKEYS.DERIVED.REPORT_ATTRIBUTES,
1042+
callback: (value) => {
1043+
if (!value) {
1044+
return;
1045+
}
1046+
reportAttributes = value;
1047+
},
1048+
});
1049+
10381050
let newGroupChatDraft: OnyxEntry<NewGroupChatDraft>;
10391051
Onyx.connect({
10401052
key: ONYXKEYS.NEW_GROUP_CHAT_DRAFT,
@@ -4510,13 +4522,6 @@ function getInvoicesChatName({
45104522
return getPolicyName({report, policy: invoiceReceiverPolicy, policies});
45114523
}
45124524

4513-
const reportNameCache = new Map<string, {lastVisibleActionCreated: string; reportName: string}>();
4514-
4515-
/**
4516-
* Get a cache key for the report name.
4517-
*/
4518-
const getCacheKey = (report: OnyxEntry<Report>): string => `${report?.reportID}-${report?.lastVisibleActionCreated}-${report?.reportName}`;
4519-
45204525
/**
45214526
* Get the title for a report using only participant names. This may be used for 1:1 DMs and other non-categorized chats.
45224527
*/
@@ -4534,6 +4539,13 @@ function buildReportNameFromParticipantNames({report, personalDetails}: {report:
45344539
.join(', ');
45354540
}
45364541

4542+
function generateReportName(report: OnyxEntry<Report>): string {
4543+
if (!report) {
4544+
return '';
4545+
}
4546+
return getReportNameInternal({report});
4547+
}
4548+
45374549
/**
45384550
* Get the title for a report.
45394551
*/
@@ -4544,6 +4556,12 @@ function getReportName(
45444556
personalDetails?: Partial<PersonalDetailsList>,
45454557
invoiceReceiverPolicy?: OnyxEntry<Policy>,
45464558
): string {
4559+
// Check if we can use report name in derived values - only when we have report but no other params
4560+
const canUseDerivedValue = report && policy === undefined && parentReportActionParam === undefined && personalDetails === undefined && invoiceReceiverPolicy === undefined;
4561+
4562+
if (canUseDerivedValue && reportAttributes?.[report.reportID]) {
4563+
return reportAttributes[report.reportID].reportName;
4564+
}
45474565
return getReportNameInternal({report, policy, parentReportActionParam, personalDetails, invoiceReceiverPolicy});
45484566
}
45494567

@@ -4573,15 +4591,6 @@ function getReportNameInternal({
45734591
policies,
45744592
}: GetReportNameParams): string {
45754593
const reportID = report?.reportID;
4576-
const cacheKey = getCacheKey(report);
4577-
4578-
if (reportID) {
4579-
const reportNameFromCache = reportNameCache.get(cacheKey);
4580-
4581-
if (reportNameFromCache?.reportName && reportNameFromCache.reportName === report?.reportName && reportNameFromCache.reportName !== CONST.REPORT.DEFAULT_REPORT_NAME) {
4582-
return reportNameFromCache.reportName;
4583-
}
4584-
}
45854594

45864595
let formattedName: string | undefined;
45874596
let parentReportAction: OnyxEntry<ReportAction>;
@@ -4765,20 +4774,12 @@ function getReportNameInternal({
47654774
}
47664775

47674776
if (formattedName) {
4768-
if (reportID) {
4769-
reportNameCache.set(cacheKey, {lastVisibleActionCreated: report?.lastVisibleActionCreated ?? '', reportName: formattedName});
4770-
}
4771-
47724777
return formatReportLastMessageText(formattedName);
47734778
}
47744779

47754780
// Not a room or PolicyExpenseChat, generate title from first 5 other participants
47764781
formattedName = buildReportNameFromParticipantNames({report, personalDetails});
47774782

4778-
if (reportID) {
4779-
reportNameCache.set(cacheKey, {lastVisibleActionCreated: report?.lastVisibleActionCreated ?? '', reportName: formattedName});
4780-
}
4781-
47824783
return formattedName;
47834784
}
47844785

@@ -10714,6 +10715,8 @@ export {
1071410715
getMovedTransactionMessage,
1071510716
getUnreportedTransactionMessage,
1071610717
getExpenseReportStateAndStatus,
10718+
generateReportName,
10719+
navigateToLinkedReportAction,
1071710720
buildOptimisticUnreportedTransactionAction,
1071810721
buildOptimisticResolvedDuplicatesReportAction,
1071910722
getTitleReportField,
@@ -10722,7 +10725,6 @@ export {
1072210725
getInvoiceReportName,
1072310726
getChatListItemReportName,
1072410727
buildOptimisticMovedTransactionAction,
10725-
navigateToLinkedReportAction,
1072610728
populateOptimisticReportFormula,
1072710729
getOutstandingReports,
1072810730
isReportOutsanding,

src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {ValueOf} from 'type-fest';
22
import ONYXKEYS from '@src/ONYXKEYS';
33
import conciergeChatReportIDConfig from './configs/conciergeChatReportID';
4+
import reportAttributesConfig from './configs/reportAttributes';
45
import type {OnyxDerivedValueConfig} from './types';
56

67
/**
@@ -9,6 +10,7 @@ import type {OnyxDerivedValueConfig} from './types';
910
*/
1011
const ONYX_DERIVED_VALUES = {
1112
[ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID]: conciergeChatReportIDConfig,
13+
[ONYXKEYS.DERIVED.REPORT_ATTRIBUTES]: reportAttributesConfig,
1214
} as const satisfies {
1315
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1416
[Key in ValueOf<typeof ONYXKEYS.DERIVED>]: OnyxDerivedValueConfig<Key, any>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import {generateReportName} from '@libs/ReportUtils';
2+
import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDerivedValueConfig';
3+
import ONYXKEYS from '@src/ONYXKEYS';
4+
import type {ReportAttributes} from '@src/types/onyx';
5+
6+
/**
7+
* This derived value is used to get the report attributes for the report.
8+
* Dependency on ONYXKEYS.PERSONAL_DETAILS_LIST is to ensure that the report attributes are generated after the personal details are available.
9+
*/
10+
11+
export default createOnyxDerivedValueConfig({
12+
key: ONYXKEYS.DERIVED.REPORT_ATTRIBUTES,
13+
dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.PERSONAL_DETAILS_LIST, ONYXKEYS.NVP_PREFERRED_LOCALE],
14+
compute: ([reports, personalDetails, preferredLocale], {currentValue, sourceValues}) => {
15+
if (!reports || !personalDetails || !preferredLocale) {
16+
return {};
17+
}
18+
19+
const reportUpdates = sourceValues?.[ONYXKEYS.COLLECTION.REPORT];
20+
21+
return Object.values(reportUpdates ?? reports).reduce<Record<string, ReportAttributes>>(
22+
(acc, report) => {
23+
if (!report) {
24+
return acc;
25+
}
26+
27+
acc[report.reportID] = {
28+
reportName: generateReportName(report),
29+
};
30+
31+
return acc;
32+
},
33+
reportUpdates && currentValue ? currentValue : {},
34+
);
35+
},
36+
});

src/libs/actions/OnyxDerived/index.ts

+24-12
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import OnyxUtils from 'react-native-onyx/dist/OnyxUtils';
1010
import Log from '@libs/Log';
1111
import ObjectUtils from '@src/types/utils/ObjectUtils';
1212
import ONYX_DERIVED_VALUES from './ONYX_DERIVED_VALUES';
13+
import type {DerivedValueContext} from './types';
1314

1415
/**
1516
* Initialize all Onyx derived values, store them in Onyx, and setup listeners to update them when dependencies change.
@@ -23,11 +24,12 @@ function init() {
2324
OnyxUtils.get(key).then((storedDerivedValue) => {
2425
let derivedValue = storedDerivedValue;
2526
if (derivedValue) {
26-
Log.info(`Derived value ${derivedValue} for ${key} restored from disk`);
27+
Log.info(`Derived value for ${key} restored from disk`);
2728
} else {
2829
OnyxUtils.tupleGet(dependencies).then((values) => {
30+
// @ts-expect-error TypeScript can't confirm the shape of tupleGet's return value matches the compute function's parameters
31+
derivedValue = compute(values, {currentValue: derivedValue});
2932
dependencyValues = values;
30-
derivedValue = compute(values, derivedValue);
3133
Onyx.set(key, derivedValue ?? null);
3234
});
3335
}
@@ -36,13 +38,23 @@ function init() {
3638
dependencyValues[i] = value;
3739
};
3840

39-
const recomputeDerivedValue = () => {
40-
const newDerivedValue = compute(dependencyValues, derivedValue);
41-
if (newDerivedValue !== derivedValue) {
42-
Log.info(`[OnyxDerived] value for key ${key} changed, updating it in Onyx`, false, {old: derivedValue ?? null, new: newDerivedValue ?? null});
43-
derivedValue = newDerivedValue;
44-
Onyx.set(key, derivedValue ?? null);
41+
const recomputeDerivedValue = (sourceKey?: string, sourceValue?: unknown) => {
42+
const context: DerivedValueContext<typeof key, typeof dependencies> = {
43+
currentValue: derivedValue,
44+
sourceValues: undefined,
45+
};
46+
47+
// If we got a source key and value, add it to the sourceValues object
48+
if (sourceKey && sourceValue !== undefined) {
49+
context.sourceValues = {
50+
[sourceKey]: sourceValue,
51+
};
4552
}
53+
// @ts-expect-error TypeScript can't confirm the shape of dependencyValues matches the compute function's parameters
54+
const newDerivedValue = compute(dependencyValues, context);
55+
Log.info(`[OnyxDerived] updating value for ${key} in Onyx`, false, {old: derivedValue ?? null, new: newDerivedValue ?? null});
56+
derivedValue = newDerivedValue;
57+
Onyx.set(key, derivedValue ?? null);
4658
};
4759

4860
for (let i = 0; i < dependencies.length; i++) {
@@ -52,10 +64,10 @@ function init() {
5264
Onyx.connect({
5365
key: dependencyOnyxKey,
5466
waitForCollectionCallback: true,
55-
callback: (value) => {
56-
Log.info(`[OnyxDerived] dependency ${dependencyOnyxKey} for derived key ${key} changed, recomputing`);
67+
callback: (value, collectionKey, sourceValue) => {
68+
Log.info(`[OnyxDerived] dependency ${collectionKey} for derived key ${key} changed, recomputing`);
5769
setDependencyValue(i, value as Parameters<typeof compute>[0][typeof i]);
58-
recomputeDerivedValue();
70+
recomputeDerivedValue(dependencyOnyxKey, sourceValue);
5971
},
6072
});
6173
} else {
@@ -64,7 +76,7 @@ function init() {
6476
callback: (value) => {
6577
Log.info(`[OnyxDerived] dependency ${dependencyOnyxKey} for derived key ${key} changed, recomputing`);
6678
setDependencyValue(i, value as Parameters<typeof compute>[0][typeof i]);
67-
recomputeDerivedValue();
79+
recomputeDerivedValue(dependencyOnyxKey);
6880
},
6981
});
7082
}

src/libs/actions/OnyxDerived/types.ts

+19-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
1-
import type {OnyxValue} from 'react-native-onyx';
1+
import type {OnyxCollection, OnyxValue} from 'react-native-onyx';
22
import type {NonEmptyTuple, ValueOf} from 'type-fest';
3-
import type {OnyxDerivedValuesMapping, OnyxKey} from '@src/ONYXKEYS';
3+
import type {OnyxCollectionKey, OnyxCollectionValuesMapping, OnyxDerivedValuesMapping, OnyxKey} from '@src/ONYXKEYS';
44
import type ONYXKEYS from '@src/ONYXKEYS';
55

6+
type OnyxCollectionSourceValue<K extends OnyxKey> = K extends OnyxCollectionKey
7+
? K extends keyof OnyxCollectionValuesMapping
8+
? OnyxCollection<OnyxCollectionValuesMapping[K]>
9+
: never
10+
: never;
11+
12+
type DerivedSourceValues<Deps extends readonly OnyxKey[]> = Partial<{
13+
[K in Deps[number]]: OnyxCollectionSourceValue<K>;
14+
}>;
15+
16+
type DerivedValueContext<Key extends OnyxKey, Deps extends NonEmptyTuple<Exclude<OnyxKey, Key>>> = {
17+
currentValue?: OnyxValue<Key>;
18+
sourceValues?: DerivedSourceValues<Deps>;
19+
};
20+
621
/**
722
* A derived value configuration describes:
823
* - a tuple of Onyx keys to subscribe to (dependencies),
@@ -17,9 +32,8 @@ type OnyxDerivedValueConfig<Key extends ValueOf<typeof ONYXKEYS.DERIVED>, Deps e
1732
args: {
1833
[Index in keyof Deps]: OnyxValue<Deps[Index]>;
1934
},
20-
currentValue: OnyxValue<Key>,
35+
context: DerivedValueContext<Key, Deps>,
2136
) => OnyxDerivedValuesMapping[Key];
2237
};
2338

24-
// eslint-disable-next-line import/prefer-default-export
25-
export type {OnyxDerivedValueConfig};
39+
export type {OnyxDerivedValueConfig, DerivedValueContext};

src/types/onyx/DerivedValues.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* The attributes of a report.
3+
*/
4+
type ReportAttributes = {
5+
/**
6+
* The name of the report.
7+
*/
8+
reportName: string;
9+
};
10+
11+
export default ReportAttributes;

src/types/onyx/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type Credentials from './Credentials';
2222
import type Currency from './Currency';
2323
import type {CurrencyList} from './Currency';
2424
import type CustomStatusDraft from './CustomStatusDraft';
25+
import type ReportAttributes from './DerivedValues';
2526
import type DismissedProductTraining from './DismissedProductTraining';
2627
import type DismissedReferralBanners from './DismissedReferralBanners';
2728
import type Download from './Download';
@@ -260,5 +261,6 @@ export type {
260261
TravelProvisioning,
261262
SidePanel,
262263
LastPaymentMethodType,
264+
ReportAttributes,
263265
TalkToAISales,
264266
};

tests/ui/GroupChatNameTests.tsx

+21-20
Original file line numberDiff line numberDiff line change
@@ -97,26 +97,27 @@ function signInAndGetApp(reportName = '', participantAccountIDs?: number[]): Pro
9797
})
9898
.then(async () => {
9999
// Simulate setting an unread report and personal details
100-
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, {
101-
reportID: REPORT_ID,
102-
reportName,
103-
lastMessageText: 'Test',
104-
participants,
105-
lastActorAccountID: USER_B_ACCOUNT_ID,
106-
type: CONST.REPORT.TYPE.CHAT,
107-
chatType: CONST.REPORT.CHAT_TYPE.GROUP,
108-
});
109-
110-
await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, {
111-
[USER_A_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_A_EMAIL, USER_A_ACCOUNT_ID, 'A'),
112-
[USER_B_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'),
113-
[USER_C_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_C_EMAIL, USER_C_ACCOUNT_ID, 'C'),
114-
[USER_D_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_D_EMAIL, USER_D_ACCOUNT_ID, 'D'),
115-
[USER_E_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_E_EMAIL, USER_E_ACCOUNT_ID, 'E'),
116-
[USER_F_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_F_EMAIL, USER_F_ACCOUNT_ID, 'F'),
117-
[USER_G_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_G_EMAIL, USER_G_ACCOUNT_ID, 'G'),
118-
[USER_H_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_H_EMAIL, USER_H_ACCOUNT_ID, 'H'),
119-
});
100+
await Promise.all([
101+
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, {
102+
reportID: REPORT_ID,
103+
reportName,
104+
lastMessageText: 'Test',
105+
participants,
106+
lastActorAccountID: USER_B_ACCOUNT_ID,
107+
type: CONST.REPORT.TYPE.CHAT,
108+
chatType: CONST.REPORT.CHAT_TYPE.GROUP,
109+
}),
110+
Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, {
111+
[USER_A_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_A_EMAIL, USER_A_ACCOUNT_ID, 'A'),
112+
[USER_B_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'),
113+
[USER_C_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_C_EMAIL, USER_C_ACCOUNT_ID, 'C'),
114+
[USER_D_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_D_EMAIL, USER_D_ACCOUNT_ID, 'D'),
115+
[USER_E_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_E_EMAIL, USER_E_ACCOUNT_ID, 'E'),
116+
[USER_F_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_F_EMAIL, USER_F_ACCOUNT_ID, 'F'),
117+
[USER_G_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_G_EMAIL, USER_G_ACCOUNT_ID, 'G'),
118+
[USER_H_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_H_EMAIL, USER_H_ACCOUNT_ID, 'H'),
119+
}),
120+
]);
120121

121122
// We manually setting the sidebar as loaded since the onLayout event does not fire in tests
122123
setSidebarLoaded();

0 commit comments

Comments
 (0)