Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -669,8 +669,8 @@
const renderItem = useCallback(
({item: reportAction, index}: ListRenderItemInfo<OnyxTypes.ReportAction>) => {
const displayAsGroup =
!isConsecutiveChronosAutomaticTimerAction(visibleReportActions, index, chatIncludesChronosWithID(reportAction?.reportID)) &&
hasNextActionMadeBySameActor(visibleReportActions, index);
!isConsecutiveChronosAutomaticTimerAction(visibleReportActions, index, chatIncludesChronosWithID(reportAction?.reportID), isOffline) &&
hasNextActionMadeBySameActor(visibleReportActions, index, isOffline);

const actionEmojiReactions = emojiReactions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportAction.reportActionID}`];
const originalReportID = getOriginalReportID(report.reportID, reportAction, reportActionsObject);
Expand Down Expand Up @@ -706,7 +706,7 @@
/>
);
},
[

Check warning on line 709 in src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

React Hook useCallback has a missing dependency: 'isOffline'. Either include it or remove the dependency array

Check warning on line 709 in src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx

View workflow job for this annotation

GitHub Actions / ESLint check

React Hook useCallback has a missing dependency: 'isOffline'. Either include it or remove the dependency array
visibleReportActions,
reportActionsObject,
parentReportAction,
Expand Down
28 changes: 14 additions & 14 deletions src/libs/ReportActionsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
}

let allReportActions: OnyxCollection<ReportActions>;
Onyx.connect({

Check warning on line 92 in src/libs/ReportActionsUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
waitForCollectionCallback: true,
callback: (actions) => {
Expand All @@ -101,7 +101,7 @@
});

let allReports: OnyxCollection<Report>;
Onyx.connect({

Check warning on line 104 in src/libs/ReportActionsUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -109,14 +109,14 @@
},
});

let isNetworkOffline = false;
let deprecatedIsNetworkOffline = false;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dukenv0307 I think we could use Onyx.connectWithoutView for this case.

cc @tgolen for confirmation

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DylanDylann can you please explain the reason?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isNetworkOffline is used by some functions that are bound directly to the UI. If we want these functions to be completely pure (as previously intended), then removing Onyx.connect like this makes sense. I just want to get confirmation from Tim to be sure that this is still the intended approach

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, thanks! I think it's still the right approach to make these pure functions.

Onyx.connect({

Check warning on line 113 in src/libs/ReportActionsUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.NETWORK,
callback: (val) => (isNetworkOffline = val?.isOffline ?? false),
callback: (val) => (deprecatedIsNetworkOffline = val?.isOffline ?? false),
});

let deprecatedCurrentUserAccountID: number | undefined;
Onyx.connect({

Check warning on line 119 in src/libs/ReportActionsUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (value) => {
// When signed out, value is undefined
Expand Down Expand Up @@ -786,13 +786,13 @@
* @param reportActions - all actions
* @param actionIndex - index of the action
*/
function findPreviousAction(reportActions: ReportAction[], actionIndex: number): OnyxEntry<ReportAction> {
function findPreviousAction(reportActions: ReportAction[], actionIndex: number, isOffline: boolean): OnyxEntry<ReportAction> {
for (let i = actionIndex + 1; i < reportActions.length; i++) {
const action = reportActions.at(i);

// Find the next non-pending deletion report action, as the pending delete action means that it is not displayed in the UI, but still is in the report actions list.
// If we are offline, all actions are pending but shown in the UI, so we take the previous action, even if it is a delete.
if (!isNetworkOffline && action?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
if (!isOffline && action?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
continue;
}

Expand All @@ -811,13 +811,13 @@
* @param reportActions - all actions
* @param actionIndex - index of the action
*/
function findNextAction(reportActions: ReportAction[], actionIndex: number): OnyxEntry<ReportAction> {
function findNextAction(reportActions: ReportAction[], actionIndex: number, isOffline: boolean): OnyxEntry<ReportAction> {
for (let i = actionIndex - 1; i >= 0; i--) {
const action = reportActions.at(i);

// Find the next non-pending deletion report action, as the pending delete action means that it is not displayed in the UI, but still is in the report actions list.
// If we are offline, all actions are pending but shown in the UI, so we take the previous action, even if it is a delete.
if (!isNetworkOffline && action?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
if (!isOffline && action?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
continue;
}

Expand All @@ -838,8 +838,8 @@
* @param reportActions - report actions ordered from latest
* @param actionIndex - index of the comment item in state to check
*/
function isConsecutiveActionMadeByPreviousActor(reportActions: ReportAction[], actionIndex: number): boolean {
const previousAction = findPreviousAction(reportActions, actionIndex);
function isConsecutiveActionMadeByPreviousActor(reportActions: ReportAction[], actionIndex: number, isOffline: boolean): boolean {
const previousAction = findPreviousAction(reportActions, actionIndex, isOffline);
const currentAction = reportActions.at(actionIndex);

return canActionsBeGrouped(currentAction, previousAction);
Expand All @@ -852,9 +852,9 @@
* @param reportActions - report actions ordered from oldest
* @param actionIndex - index of the comment item in state to check
*/
function hasNextActionMadeBySameActor(reportActions: ReportAction[], actionIndex: number) {
function hasNextActionMadeBySameActor(reportActions: ReportAction[], actionIndex: number, isOffline: boolean) {
const currentAction = reportActions.at(actionIndex);
const nextAction = findNextAction(reportActions, actionIndex);
const nextAction = findNextAction(reportActions, actionIndex, isOffline);

if (actionIndex === 0) {
return false;
Expand Down Expand Up @@ -989,8 +989,8 @@
* If the user sends consecutive actions to Chronos to automatically start/stop the timer,
* then detect that and show each individually so that the user can easily see when they were sent.
*/
function isConsecutiveChronosAutomaticTimerAction(reportActions: ReportAction[], actionIndex: number, isChronosReport: boolean): boolean {
const previousAction = findPreviousAction(reportActions, actionIndex);
function isConsecutiveChronosAutomaticTimerAction(reportActions: ReportAction[], actionIndex: number, isChronosReport: boolean, isOffline: boolean): boolean {
const previousAction = findPreviousAction(reportActions, actionIndex, isOffline);
const currentAction = reportActions?.at(actionIndex);
return isChronosAutomaticTimerAction(currentAction, isChronosReport) && isChronosAutomaticTimerAction(previousAction, isChronosReport);
}
Expand Down Expand Up @@ -1202,7 +1202,7 @@
if (!reportAction) {
return true;
}
return !isNetworkOffline && reportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
return !deprecatedIsNetworkOffline && reportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
}

/**
Expand Down Expand Up @@ -1832,7 +1832,7 @@
iouRequestTypesSet.has(actionType) &&
!!originalMessage?.IOUTransactionID &&
// Include deleted IOU reportActions if the action is pending deletion and the user is offline
(!isDeletedAction(action) || (action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && (isOffline ?? isNetworkOffline)))
(!isDeletedAction(action) || (action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && (isOffline ?? deprecatedIsNetworkOffline)))
) {
if (iouRequestAction !== null) {
// We found a second action so this is for sure not a one-transaction report
Expand Down
4 changes: 2 additions & 2 deletions src/pages/inbox/report/ReportActionsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -748,8 +748,8 @@
transactionThreadReport={transactionThreadReport}
linkedReportActionID={linkedReportActionID}
displayAsGroup={
!isConsecutiveChronosAutomaticTimerAction(sortedVisibleReportActions, index, chatIncludesChronosWithID(reportAction?.reportID)) &&
isConsecutiveActionMadeByPreviousActor(sortedVisibleReportActions, index)
!isConsecutiveChronosAutomaticTimerAction(sortedVisibleReportActions, index, chatIncludesChronosWithID(reportAction?.reportID), isOffline) &&
isConsecutiveActionMadeByPreviousActor(sortedVisibleReportActions, index, isOffline)
}
mostRecentIOUReportActionID={mostRecentIOUReportActionID}
shouldHideThreadDividerLine={shouldHideThreadDividerLine}
Expand All @@ -773,7 +773,7 @@
</>
);
},
[

Check warning on line 776 in src/pages/inbox/report/ReportActionsList.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

React Hook useCallback has a missing dependency: 'isOffline'. Either include it or remove the dependency array
draftMessage,
emojiReactions,
parentReportAction,
Expand Down
117 changes: 117 additions & 0 deletions tests/unit/ReportActionsUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ import {
getSortedReportActions,
getSortedReportActionsForDisplay,
getUpdateACHAccountMessage,
hasNextActionMadeBySameActor,
hasReasoning,
isConsecutiveActionMadeByPreviousActor,
isConsecutiveChronosAutomaticTimerAction,
isIOUActionMatchingTransactionList,
isNewerReportAction,
isReportActionVisibleAsLastAction,
Expand All @@ -44,6 +47,7 @@ import createRandomReportAction from '../utils/collections/reportActions';
import {createRandomReport} from '../utils/collections/reports';
import createRandomTransaction from '../utils/collections/transaction';
import * as LHNTestUtils from '../utils/LHNTestUtils';
import {getFakeReportAction} from '../utils/ReportTestUtils';
import {translateLocal} from '../utils/TestHelper';
import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates';
Expand Down Expand Up @@ -4126,4 +4130,117 @@ describe('ReportActionsUtils', () => {
expect(hasReasoning(undefined)).toBe(false);
});
});

describe('isConsecutiveActionMadeByPreviousActor', () => {
const accountID = 1;

it('returns false if current action is missing', () => {
expect(isConsecutiveActionMadeByPreviousActor([getFakeReportAction(accountID)], 0, false)).toBe(false);
});

it('returns false if actions are more than 5 minutes apart', () => {
const actions = [getFakeReportAction(accountID, {created: '2025-01-01T02:00:00Z'}), getFakeReportAction(accountID, {created: '2025-01-01T01:01:00Z'})];
expect(isConsecutiveActionMadeByPreviousActor(actions, 0, false)).toBe(false);
});

it('returns true when same actor and within 5 minutes', () => {
const actions = [
getFakeReportAction(accountID, {actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT}),
getFakeReportAction(accountID, {actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT}),
];
expect(isConsecutiveActionMadeByPreviousActor(actions, 0, false)).toBe(true);
});

it('skips pending-delete actions when online', () => {
const actions = [getFakeReportAction(accountID), getFakeReportAction(accountID, {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}), getFakeReportAction(accountID)];
expect(isConsecutiveActionMadeByPreviousActor(actions, 0, false)).toBe(true);
});

it('does not skip pending-delete actions when offline', () => {
const actions = [getFakeReportAction(accountID), getFakeReportAction(2, {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}), getFakeReportAction(accountID)];
expect(isConsecutiveActionMadeByPreviousActor(actions, 0, true)).toBe(false);
});
});

describe('hasNextActionMadeBySameActor', () => {
const accountID = 1;

it('returns false if inspecting first item on the list', () => {
expect(hasNextActionMadeBySameActor([], 0, false)).toBe(false);
});

it('returns false if actions are more than 5 minutes apart', () => {
const actions = [getFakeReportAction(accountID, {created: '2025-01-01T01:01:00Z'}), getFakeReportAction(accountID, {created: '2025-01-01T02:00:00Z'})];
expect(hasNextActionMadeBySameActor(actions, 1, false)).toBe(false);
});

it('returns true when same actor and within 5 minutes', () => {
const actions = [
getFakeReportAction(accountID, {actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT}),
getFakeReportAction(accountID, {actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT}),
];
expect(hasNextActionMadeBySameActor(actions, 1, false)).toBe(true);
});

it('skips pending-delete actions when online', () => {
const actions = [getFakeReportAction(accountID), getFakeReportAction(accountID, {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}), getFakeReportAction(accountID)];
expect(hasNextActionMadeBySameActor(actions, 2, false)).toBe(true);
});

it('does not skip pending-delete actions when offline', () => {
const actions = [getFakeReportAction(accountID), getFakeReportAction(2, {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}), getFakeReportAction(accountID)];
expect(hasNextActionMadeBySameActor(actions, 2, true)).toBe(false);
});
});

describe('isConsecutiveChronosAutomaticTimerAction', () => {
const accountID = 1;

function makeChronosAction(text: string, overrides: Parameters<typeof getFakeReportAction>[1] = {}) {
return getFakeReportAction(accountID, {
message: [{html: text, isDeletedParentAction: false, isEdited: false, text, type: 'TEXT', whisperedTo: []}],
...overrides,
});
}

it('returns false when isChronosReport is false', () => {
const actions = [makeChronosAction('started'), makeChronosAction('started')];
expect(isConsecutiveChronosAutomaticTimerAction(actions, 0, false, false)).toBe(false);
});

it('returns false when current action is not a timer action', () => {
const actions = [makeChronosAction('hello'), makeChronosAction('started')];
expect(isConsecutiveChronosAutomaticTimerAction(actions, 0, true, false)).toBe(false);
});

it('returns false when previous action is not a timer action', () => {
const actions = [makeChronosAction('started'), makeChronosAction('hello')];
expect(isConsecutiveChronosAutomaticTimerAction(actions, 0, true, false)).toBe(false);
});

it('returns true when both current and previous are timer actions', () => {
const actions = [makeChronosAction('started'), makeChronosAction('stopped')];
expect(isConsecutiveChronosAutomaticTimerAction(actions, 0, true, false)).toBe(true);
});

it('returns false when there is no previous action', () => {
const actions = [makeChronosAction('started')];
expect(isConsecutiveChronosAutomaticTimerAction(actions, 0, true, false)).toBe(false);
});

it('skips pending-delete previous action when online', () => {
const actions = [makeChronosAction('started'), makeChronosAction('stopped', {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}), makeChronosAction('started')];
expect(isConsecutiveChronosAutomaticTimerAction(actions, 0, true, false)).toBe(true);
});

it('does not skip pending-delete previous action when offline', () => {
const actions = [makeChronosAction('hello'), makeChronosAction('stopped', {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}), makeChronosAction('started')];
expect(isConsecutiveChronosAutomaticTimerAction(actions, 0, true, true)).toBe(false);
});

it('includes pending-delete timer action as previous when offline', () => {
const actions = [makeChronosAction('started'), makeChronosAction('stopped', {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}), makeChronosAction('started')];
expect(isConsecutiveChronosAutomaticTimerAction(actions, 0, true, true)).toBe(true);
});
});
});
Loading
Loading