Skip to content
Draft
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
4 changes: 2 additions & 2 deletions __mocks__/react-native-onyx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import type {ConnectOptions, OnyxKey} from 'react-native-onyx';

// eslint-disable-next-line no-restricted-imports
import Onyx, {useOnyx} from 'react-native-onyx';
import Onyx, {useOnyx, useOnyxState} from 'react-native-onyx';

let connectCallbackDelay = 0;
function addDelayToConnectCallback(delay: number) {
Expand Down Expand Up @@ -41,4 +41,4 @@ const reactNativeOnyxMock: ReactNativeOnyxMock = {

export default reactNativeOnyxMock;

export {useOnyx};
export {useOnyx, useOnyxState};
2 changes: 0 additions & 2 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1444,7 +1444,6 @@ const ONYXKEYS = {
LOGIN_TO_ACCOUNT_ID_MAP: 'loginToAccountIDMap',
GUIDE_ACCOUNT_IDS: 'guideAccountIDs',
SPEND_DATA_SIGNATURE: 'spendDataSignature',
ONE_ON_ONE_CHAT_REPORT_IDS: 'oneOnOneChatReportIDs',
},

/** Stores HybridApp specific state required to interoperate with OldDot */
Expand Down Expand Up @@ -2027,7 +2026,6 @@ type OnyxDerivedValuesMapping = {
[ONYXKEYS.DERIVED.LOGIN_TO_ACCOUNT_ID_MAP]: OnyxTypes.LoginToAccountIDMapDerivedValue;
[ONYXKEYS.DERIVED.SPEND_DATA_SIGNATURE]: OnyxTypes.SpendDataSignatureDerivedValue;
[ONYXKEYS.DERIVED.GUIDE_ACCOUNT_IDS]: OnyxTypes.GuideAccountIDsDerivedValue;
[ONYXKEYS.DERIVED.ONE_ON_ONE_CHAT_REPORT_IDS]: OnyxTypes.OneOnOneChatReportIDsDerivedValue;
};

type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping & OnyxDerivedValuesMapping;
Expand Down
7 changes: 5 additions & 2 deletions src/components/Table/TableBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ function TableBodyList({contentContainerStyle, emptyMessage, onLayout, style, ..
noResultsStateElement,
tableListMetadata,
isEmptyResult,
isDefaultViewEmpty,
scrollWidth,
tableWidth,
} = useTableContext<TableData>();
Expand Down Expand Up @@ -337,8 +338,10 @@ function TableBodyList({contentContainerStyle, emptyMessage, onLayout, style, ..
</View>
);

const emptyStateContent =
tableListMetadata.hasPageHeader && isEmptyResult ? (noResultsStateElement ?? EmptyResultComponent) : (emptyStateElement ?? renderListComponent(ListEmptyComponent));
// When the default view hides every row (e.g. all workspaces are archived), no search or filter is active,
// so show the empty state rather than "No results".
const shouldShowNoResultsState = tableListMetadata.hasPageHeader && isEmptyResult && !isDefaultViewEmpty;
const emptyStateContent = shouldShowNoResultsState ? (noResultsStateElement ?? EmptyResultComponent) : (emptyStateElement ?? renderListComponent(ListEmptyComponent));
const footerElement = renderListComponent(ListFooterComponent);
// Consumer footer flex growth is useful below normal rows, but inside the combined empty-state
// footer it can expand over the page header. Preserve the remaining style while disabling growth.
Expand Down
5 changes: 3 additions & 2 deletions src/components/Table/TableEmptyStates/TableNoResultsState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ type TableNoResultsStateProps = Omit<GenericEmptyStateComponentProps, 'headerMed
export default function TableNoResultsState(emptyStateProps: TableNoResultsStateProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {isEmptyResult, tableListMetadata} = useTableContext();
const {isEmptyResult, isDefaultViewEmpty, tableListMetadata} = useTableContext();
const illustrations = useMemoizedLazyIllustrations(['EmptyShelves']);

if (!isEmptyResult) {
// The default view hiding every row is not a search/filter result, so the empty state handles it instead.
if (!isEmptyResult || isDefaultViewEmpty) {
return null;
}

Expand Down
27 changes: 27 additions & 0 deletions src/hooks/useOneOnOneChatReportID.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import ONYXKEYS from '@src/ONYXKEYS';
import type {Report} from '@src/types/onyx';

import type {OnyxCollection} from 'react-native-onyx';

import {getOneOnOneChatReportID} from '@selectors/OneOnOneChatReportID';
import {useOnyxState} from 'react-native-onyx';

const ONE_ON_ONE_CHAT_REPORT_ID_DEPENDENCIES = [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.SESSION];

/** Returns the reportID of the 1:1 or system chat with exactly these participants, read live so the Search snapshot never applies. */
function useOneOnOneChatReportID(participantsChatKey: string | undefined): string | undefined {
return useOnyxState(
(state) => {
if (!participantsChatKey) {
return undefined;
}
// useOnyxState's state view types a collection key as one member, but at runtime it returns the whole collection
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const reports = state[ONYXKEYS.COLLECTION.REPORT] as OnyxCollection<Report>;
return getOneOnOneChatReportID(reports, state[ONYXKEYS.SESSION]?.accountID, participantsChatKey);
},
{dependencies: ONE_ON_ONE_CHAT_REPORT_ID_DEPENDENCIES},
);
}

export default useOneOnOneChatReportID;
2 changes: 1 addition & 1 deletion src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11256,7 +11256,7 @@ function shouldReportBeInOptionList(params: ShouldReportBeInOptionListParams) {
}

/**
* Stable key for a participant set, used by the `ONE_ON_ONE_CHAT_REPORT_IDS` derived value.
* Stable key for a participant set, used to index 1:1 chats by participants.
*
* Sorting uses the default comparator to match `getChatByParticipants`, which compares `.sort()`ed number arrays.
* That sort is lexicographic: [2, 10] becomes [10, 2]. A numeric sort here would stop matching.
Expand Down
2 changes: 0 additions & 2 deletions src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import cardFeedErrorsConfig from './configs/cardFeedErrors';
import guideAccountIDsConfig from './configs/guideAccountIDs';
import loginToAccountIDMapConfig from './configs/loginToAccountIDMap';
import nonPersonalAndWorkspaceCardListConfig from './configs/nonPersonalAndWorkspaceCardList';
import oneOnOneChatReportIDsConfig from './configs/oneOnOneChatReportIDs';
import outstandingReportsByPolicyIDConfig from './configs/outstandingReportsByPolicyID';
import personalAndWorkspaceCardListConfig from './configs/personalAndWorkspaceCardList';
import reportAttributesConfig from './configs/reportAttributes';
Expand All @@ -33,7 +32,6 @@ const ONYX_DERIVED_VALUES = {
[ONYXKEYS.DERIVED.LOGIN_TO_ACCOUNT_ID_MAP]: loginToAccountIDMapConfig,
[ONYXKEYS.DERIVED.GUIDE_ACCOUNT_IDS]: guideAccountIDsConfig,
[ONYXKEYS.DERIVED.SPEND_DATA_SIGNATURE]: spendDataSignatureConfig,
[ONYXKEYS.DERIVED.ONE_ON_ONE_CHAT_REPORT_IDS]: oneOnOneChatReportIDsConfig,
} as const satisfies {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[Key in ValueOf<typeof ONYXKEYS.DERIVED>]: OnyxDerivedValueConfig<Key, any>;
Expand Down
105 changes: 0 additions & 105 deletions src/libs/actions/OnyxDerived/configs/oneOnOneChatReportIDs.ts

This file was deleted.

10 changes: 3 additions & 7 deletions src/pages/iou/request/step/IOURequestStepConfirmation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useOdometerReceiptStitcher from '@hooks/useOdometerReceiptStitcher';
import useOneOnOneChatReportID from '@hooks/useOneOnOneChatReportID';
import useOnyx from '@hooks/useOnyx';
import useOptimisticDraftTransactions from '@hooks/useOptimisticDraftTransactions';
import useParticipantsPolicies from '@hooks/useParticipantsPolicies';
Expand Down Expand Up @@ -94,7 +95,7 @@ import type {IOUType} from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
import type {OneOnOneChatReportIDsDerivedValue, Policy} from '@src/types/onyx';
import type {Policy} from '@src/types/onyx';
import type {Participant} from '@src/types/onyx/IOU';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import type {Receipt} from '@src/types/onyx/Transaction';
Expand Down Expand Up @@ -722,12 +723,7 @@ function IOURequestStepConfirmationContent({
const p2pRecipientAccountID = firstParticipant?.accountID ?? CONST.DEFAULT_NUMBER_ID;

// Read reports reactively: if the chat lands mid-flow the optimistic ID must drop out, or we'd reveal an uncreated report.
const existingP2PChatSelector = useCallback(
(chatReportIDs: OnyxEntry<OneOnOneChatReportIDsDerivedValue>) =>
isP2PDestination ? chatReportIDs?.reportIDs?.[getParticipantsChatKey([p2pRecipientAccountID, currentUserPersonalDetails.accountID])] : undefined,
[isP2PDestination, p2pRecipientAccountID, currentUserPersonalDetails.accountID],
);
const [existingP2PDestinationReportID] = useOnyx(ONYXKEYS.DERIVED.ONE_ON_ONE_CHAT_REPORT_IDS, {selector: existingP2PChatSelector});
const existingP2PDestinationReportID = useOneOnOneChatReportID(isP2PDestination ? getParticipantsChatKey([p2pRecipientAccountID, currentUserPersonalDetails.accountID]) : undefined);
const optimisticP2PDestinationReportID = !existingP2PDestinationReportID && reusableP2PReportID ? reusableP2PReportID : undefined;
// Trust `report` when it already belongs to this participant (their chat, or an IOU report under it), so a
// flow started from an IOU report keeps that report as destination instead of falling back to the chat.
Expand Down
66 changes: 66 additions & 0 deletions src/selectors/OneOnOneChatReportID.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {getParticipantsChatKey, isOneOnOneChat, isSystemChat} from '@libs/ReportUtils';

import type {Report} from '@src/types/onyx';

import type {OnyxCollection} from 'react-native-onyx';

const lookupCache = new WeakMap<NonNullable<OnyxCollection<Report>>, Map<string, string | undefined>>();

function isOneOnOneChatWithParticipants(report: Report, currentUserAccountID: number, participantsChatKey: string, participantAccountIDs: string[]): boolean {
const participants = report.participants;
if (!report.reportID || !participants) {
return false;
}
for (const accountID of participantAccountIDs) {
if (!(accountID in participants)) {
return false;
}
}
if (!isOneOnOneChat(report, currentUserAccountID) && !isSystemChat(report)) {
return false;
}
return getParticipantsChatKey(Object.keys(participants).map(Number)) === participantsChatKey;
}

/**
* Finds the 1:1 or system chat whose participants are exactly the ones in `participantsChatKey` (built with
* `getParticipantsChatKey`). The first report in collection order wins, matching `getChatByParticipants`. Group chats
* are left out because `getChatByParticipants` only returns those behind `shouldIncludeGroupChats`.
*
* Reports that miss one of the participants are skipped before the 1:1 checks run, and the answer is cached on the
* collection snapshot, so re-renders without a report write cost a lookup.
*/
function getOneOnOneChatReportID(reports: OnyxCollection<Report>, currentUserAccountID: number | undefined, participantsChatKey: string): string | undefined {
// Without the accountID every DM looks like a group chat.
if (!reports || !currentUserAccountID) {
return undefined;
}

let cachedLookups = lookupCache.get(reports);
const cacheKey = `${currentUserAccountID}:${participantsChatKey}`;
if (cachedLookups?.has(cacheKey)) {
return cachedLookups.get(cacheKey);
}

const participantAccountIDs = participantsChatKey.split(',');
let reportID: string | undefined;
// for...in walks the frozen snapshot without copying thousands of reports into an array, which is most of the cost of a lookup on mount
// eslint-disable-next-line guard-for-in
for (const reportKey in reports) {
const report = reports[reportKey];
if (report && isOneOnOneChatWithParticipants(report, currentUserAccountID, participantsChatKey, participantAccountIDs)) {
reportID = report.reportID;
break;
}
}

if (!cachedLookups) {
cachedLookups = new Map();
lookupCache.set(reports, cachedLookups);
}
cachedLookups.set(cacheKey, reportID);
return reportID;
}

// eslint-disable-next-line import/prefer-default-export
export {getOneOnOneChatReportID};
9 changes: 0 additions & 9 deletions src/types/onyx/DerivedValues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,14 +236,6 @@ type SpendDataSignatureDerivedValue = {
cardExpenses: number;
};

/**
* Maps a participant set to the reportID of its 1:1 (or system) chat, for the accountID it was built for.
*/
type OneOnOneChatReportIDsDerivedValue = {
reportIDs: Record<string, string>;
accountID?: number;
};

export type {
ReportAttributes,
ReportAttributesDerivedValue,
Expand All @@ -258,7 +250,6 @@ export type {
LoginToAccountIDMapDerivedValue,
GuideAccountIDsDerivedValue,
SpendDataSignatureDerivedValue,
OneOnOneChatReportIDsDerivedValue,
CardFeedErrorsObject,
CardFeedErrorState,
CardFeedErrors,
Expand Down
2 changes: 0 additions & 2 deletions src/types/onyx/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ import type {
SpendDataSignatureDerivedValue,
LoginToAccountIDMapDerivedValue,
NonPersonalAndWorkspaceCardListDerivedValue,
OneOnOneChatReportIDsDerivedValue,
OutstandingReportsByPolicyIDDerivedValue,
PersonalAndWorkspaceCardListDerivedValue,
ReportAttributesDerivedValue,
Expand Down Expand Up @@ -504,7 +503,6 @@ export type {
LoginToAccountIDMapDerivedValue,
GuideAccountIDsDerivedValue,
SpendDataSignatureDerivedValue,
OneOnOneChatReportIDsDerivedValue,
ScheduleCallDraft,
ValidateUserAndGetAccessiblePolicies,
VacationDelegate,
Expand Down
Loading