Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 4 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"type-fest": "4.20.0",
"type-fest": "4.34.1",
"typescript": "^5.4.5",
"wait-port": "^0.2.9",
"webpack": "^5.94.0",
Expand Down
24 changes: 22 additions & 2 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import type CONST from './CONST';
import type {OnboardingCompanySize} from './CONST';
Expand Down Expand Up @@ -1088,14 +1089,33 @@ type OnyxCollectionKey = keyof OnyxCollectionValuesMapping;
type OnyxFormKey = keyof OnyxFormValuesMapping;
type OnyxFormDraftKey = keyof OnyxFormDraftValuesMapping;
type OnyxValueKey = keyof OnyxValuesMapping;

type OnyxKey = OnyxValueKey | OnyxCollectionKey | OnyxFormKey | OnyxFormDraftKey;
type OnyxPagesKey = typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES;

type GetOnyxTypeForKey<K extends OnyxKey> =
// Forms (and draft forms) behave like value keys
K extends OnyxFormKey
? OnyxEntry<OnyxFormValuesMapping[K]>
: K extends OnyxFormDraftKey
? OnyxEntry<OnyxFormDraftValuesMapping[K]>
: // Plain non-collection values
K extends OnyxValueKey
? OnyxEntry<OnyxValuesMapping[K]>
: // Exactly matching a collection key returns a collection
K extends OnyxCollectionKey
? OnyxCollection<OnyxCollectionValuesMapping[K]>
: // Otherwise, if K is a string that starts with one of the collection keys,
// return an entry for that collection’s value type.
K extends string
? {
[X in OnyxCollectionKey]: K extends `${X}${string}` ? OnyxEntry<OnyxCollectionValuesMapping[X]> : never;
}[OnyxCollectionKey]
: never;

type MissingOnyxKeysError = `Error: Types don't match, OnyxKey type is missing: ${Exclude<AllOnyxKeys, OnyxKey>}`;
/** If this type errors, it means that the `OnyxKey` type is missing some keys. */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type AssertOnyxKeys = AssertTypesEqual<AllOnyxKeys, OnyxKey, MissingOnyxKeysError>;

export default ONYXKEYS;
export type {OnyxCollectionKey, OnyxCollectionValuesMapping, OnyxFormDraftKey, OnyxFormKey, OnyxFormValuesMapping, OnyxKey, OnyxPagesKey, OnyxValueKey, OnyxValues};
export type {OnyxCollectionKey, OnyxCollectionValuesMapping, OnyxFormDraftKey, OnyxFormKey, OnyxFormValuesMapping, OnyxKey, OnyxPagesKey, OnyxValueKey, OnyxValues, GetOnyxTypeForKey};
18 changes: 18 additions & 0 deletions src/hooks/useOnyxDerivedValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {useSyncExternalStore} from 'react';
import type {ValueOf} from 'type-fest';
import type {ONYX_DERIVED_VALUES} from '@libs/OnyxDerived';
import OnyxDerived from '@libs/OnyxDerived';

/**
* Hook that subscribes to a derived Onyx value.
*
* Pass in one of the derived configs from ONYX_DERIVED_VALUES.
* This hook uses React's useSyncExternalStore to subscribe to the derived store,
* so that your component re-renders only when the derived value changes.
*/
function useDerivedOnyxValue(config: ValueOf<typeof ONYX_DERIVED_VALUES>): ReturnType<typeof config.compute> {
const store = OnyxDerived.getDerivedValueStore(config);
return useSyncExternalStore(store.subscribe, () => store.currentValue);
}

export default useDerivedOnyxValue;
199 changes: 199 additions & 0 deletions src/libs/OnyxDerived.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import Onyx from 'react-native-onyx';
import OnyxUtils from 'react-native-onyx/dist/OnyxUtils';
import type {NonEmptyTuple, ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import type {GetOnyxTypeForKey, OnyxKey} from '@src/ONYXKEYS';
import ONYXKEYS from '@src/ONYXKEYS';
import {isThread} from './ReportUtils';

/**
* A derived value configuration describes:
* - a unique key,
* - a tuple of Onyx keys to subscribe to (dependencies),
* - a compute function that derives a value from the dependent Onyx values.
* The compute function receives a single argument that's a tuple of the onyx values for the declared dependencies.
* For example, if your dependencies are `['report_', 'account'], then compute will receive a [OnyxCollection<Report>, OnyxEntry<Account>]
*/
type OnyxDerivedValueConfig<Key extends string, Deps extends NonEmptyTuple<OnyxKey>> = {
key: Key;
dependencies: Deps;
compute: (args: {
-readonly [Index in keyof Deps]: GetOnyxTypeForKey<Deps[Index]>;
}) => unknown;
};

/**
* Helper function to create a derived value config. This function is just here to help TypeScript infer Deps, so instead of writing this:
*
* const conciergeChatReportIDConfig: OnyxDerivedValueConfig<typeof ONYXKEYS.COLLECTION.REPORT, typeof ONYXKEYS.CONCIERGE_REPORT_ID> = {
* dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.CONCIERGE_REPORT_ID],
* ...
* };
*
* We can just write this:
*
* const conciergeChatReportIDConfig = createOnyxDerivedValueConfig({
* dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.CONCIERGE_REPORT_ID]
* })
*/
function createOnyxDerivedValueConfig<Key extends string, Deps extends NonEmptyTuple<OnyxKey>>(config: OnyxDerivedValueConfig<Key, Deps>): OnyxDerivedValueConfig<Key, Deps> {
return config;
}

/**
* Global map of derived configs.
* This object holds our derived value configurations.
*/
const ONYX_DERIVED_VALUES = {
CONCIERGE_CHAT_REPORT_ID: createOnyxDerivedValueConfig({
key: 'conciergeChatReportID',
dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.CONCIERGE_REPORT_ID],
compute: ([reports, conciergeChatReportID]) => {
if (!reports) {
return null;
}

const conciergeReport = Object.values(reports).find((report) => {
if (!report?.participants || isThread(report)) {
return false;
}

const participantAccountIDs = new Set(Object.keys(report.participants));
if (participantAccountIDs.size !== 2) {
return false;
}

return participantAccountIDs.has(CONST.ACCOUNT_ID.CONCIERGE.toString()) || report?.reportID === conciergeChatReportID;
});

return conciergeReport?.reportID;
},
}),
} as const;

/**
* Interface for a derived value store.
*
* A derived value store holds the current derived value and a subscribe function.
* When any dependency updates, the store recalculates the derived value and notifies its subscribers.
*/
type DerivedValueStore<T> = {
currentValue: T;
subscribe: (cb: () => void) => () => void;
};

/**
* Get the union of all derived config keys from our ONYX_DERIVED_VALUES map.
*/
type DerivedValueStoreKey = ValueOf<typeof ONYX_DERIVED_VALUES>['key'];

/**
* This type maps each config key (from ONYX_DERIVED_VALUES)
* to a DerivedValueStore whose currentValue type is the return type of that config's compute function.
*/
type DerivedValueStoreMap = {
[K in DerivedValueStoreKey]: DerivedValueStore<
ReturnType<
// Extract from the union of derived configs the one with key K, then get its compute function.
Extract<ValueOf<typeof ONYX_DERIVED_VALUES>, {key: K}>['compute']
>
>;
};

/**
* Global object holding our derived value stores.
* We use a Partial so that we can lazily create each store when needed.
*/
const derivedValueStores: Partial<DerivedValueStoreMap> = {};

/**
* Creates a new derived value store for a given config.
*
* For each dependency key in the config, we create an array (currentOnyxValues)
* that will hold the latest Onyx values. Then we call Onyx.connect for each dependency,
* updating the corresponding entry in currentOnyxValues whenever the value changes.
* After each update, we recalculate the derived value using the config.compute function.
*/
function createDerivedValueStore(config: ValueOf<typeof ONYX_DERIVED_VALUES>): DerivedValueStore<ReturnType<typeof config.compute>> {
// Create an array to hold the current values for each dependency.
// We cast its type to match the tuple expected by config.compute.
const currentOnyxValues = new Array(config.dependencies.length) as Parameters<typeof config.compute>[0];
let derivedValue = config.compute(currentOnyxValues);

const subscribers = new Set<() => void>();

// Function to re-calculate the derived value and notify subscribers if it changes.
function recomputeValue() {
const newDerivedValue = config.compute(currentOnyxValues);
// If the derived value has changed, notify all subscribers.
if (newDerivedValue !== derivedValue) {
derivedValue = newDerivedValue;
for (const subscriber of subscribers) {
subscriber();
}
}
}

// Create Onyx subscriptions for each dependency.
const connections = config.dependencies.map((onyxKey, index) =>
OnyxUtils.isCollectionKey(onyxKey)
? Onyx.connect({
key: onyxKey,
waitForCollectionCallback: true,
callback: (value) => {
currentOnyxValues[index] = value;
recomputeValue();
},
})
: Onyx.connect({
key: onyxKey,
callback: (value) => {
currentOnyxValues[index] = value;
recomputeValue();
},
}),
);

return {
get currentValue() {
return derivedValue;
},
subscribe(cb: () => void) {
subscribers.add(cb);
return () => {
subscribers.delete(cb);
if (subscribers.size === 0) {
for (const connection of connections) {
Onyx.disconnect(connection);
}
}
};
},
};
}

/**
* Retrieves (or creates if necessary) the derived value store for the given config.
*
* We use the config's literal key to index into our global derivedValueStores object.
* If a store for that key doesn't exist yet, we create it.
*/
function getDerivedValueStore(config: ValueOf<typeof ONYX_DERIVED_VALUES>) {
let store = derivedValueStores[config.key];
if (!store) {
store = createDerivedValueStore(config);
derivedValueStores[config.key] = store;
}
return store;
}

function get(config: ValueOf<typeof ONYX_DERIVED_VALUES>) {
const store = getDerivedValueStore(config);
return store.currentValue;
}

export {ONYX_DERIVED_VALUES};
export default {
get,
getDerivedValueStore,
};
19 changes: 2 additions & 17 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {FallbackAvatar, IntacctSquare, NetSuiteSquare, NSQSSquare, QBOSquare, Xe
import * as defaultGroupAvatars from '@components/Icon/GroupDefaultAvatars';
import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars';
import type {MoneyRequestAmountInputProps} from '@components/MoneyRequestAmountInput';
import OnyxDerived, {ONYX_DERIVED_VALUES} from '@libs/OnyxDerived';
import type {IOUAction, IOUType, OnboardingPurpose} from '@src/CONST';
import CONST from '@src/CONST';
import type {ParentNavigationSummaryParams} from '@src/languages/params';
Expand Down Expand Up @@ -728,12 +729,6 @@ let isAnonymousUser = false;
// Example case: when we need to get a report name of a thread which is dependent on a report action message.
const parsedReportActionMessageCache: Record<string, string> = {};

let conciergeChatReportID: string | undefined;
Onyx.connect({
key: ONYXKEYS.CONCIERGE_REPORT_ID,
callback: (value) => (conciergeChatReportID = value),
});

const defaultAvatarBuildingIconTestID = 'SvgDefaultAvatarBuilding Icon';
Onyx.connect({
key: ONYXKEYS.SESSION,
Expand Down Expand Up @@ -1519,21 +1514,11 @@ function getReportNotificationPreference(report: OnyxEntry<Report>): ValueOf<typ
return participant?.notificationPreference ?? CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
}

const CONCIERGE_ACCOUNT_ID_STRING = CONST.ACCOUNT_ID.CONCIERGE.toString();
/**
* Only returns true if this is our main 1:1 DM report with Concierge.
*/
function isConciergeChatReport(report: OnyxInputOrEntry<Report>): boolean {
if (!report?.participants || isThread(report)) {
return false;
}

const participantAccountIDs = new Set(Object.keys(report.participants));
if (participantAccountIDs.size !== 2) {
return false;
}

return participantAccountIDs.has(CONCIERGE_ACCOUNT_ID_STRING) || report?.reportID === conciergeChatReportID;
return !!report && report?.reportID === OnyxDerived.get(ONYX_DERIVED_VALUES.CONCIERGE_CHAT_REPORT_ID);
}

function findSelfDMReportID(): string | undefined {
Expand Down