Skip to content

[WIP] [HOLD on #104977, #104978] [LegendList 6/9] feat: migrate report actions to chronological LegendList - #100733

Open
chrispader wants to merge 95 commits into
Expensify:mainfrom
margelo:@chrispader/feat/legend-list-report-actions
Open

chrispader wants to merge 95 commits into
Expensify:mainfrom
margelo:@chrispader/feat/legend-list-report-actions

Conversation

@chrispader

@chrispader chrispader commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

The report-actions list currently uses inversion, so rendering order and scroll coordinates differ from chronological message order. This PR moves the main chat list to LegendList and updates pagination, unread tracking, message editing, recycling, and initial loading behavior to work with chronological data.

@roryabraham @dmkt9

@Expensify/design Please review the initial chat-loading skeleton shown below.

Explanation of Change

This is layer 6 of the nine-part LegendList series. It moves the main report-actions list to chronological LegendList data and updates pagination, unread tracking, linked-message positioning, end following, and the initial loading cover. LegendList 3.7.0 includes the upstream Fabric content-size fix. The Android end-clamp and React Native scroll-preservation patches remain with this migration.

Composer focus and notification preparation are in layer 4, #104977. Shared list refs, row state, and money-report edit scrolling are in layer 5, #104978. Both preparations must merge before this PR.

Review the 34-file migration-only diff. The Files changed tab includes the preparation work until both PRs merge and main is synced. The only shared file in the isolated diffs is the temporary FlashList type export, which layer 5 adds for compatibility and this layer removes with the inverted wrapper.

Review and merge order: nine-part LegendList rollout. Layers 4 and 5 are independent preparations; merge both before layer 6.

Fixed Issues

$ #98994
$ #99202

This PR is part of the migration split from #98270. No approved proposal is linked.

Tests

  1. Sign in with a high-traffic account and open a chat with enough history to paginate in both directions. Test both a cold open and a warm open, including an expense chat. Verify there is one consistently positioned loading skeleton with bottom padding before the content appears.
  2. Scroll through older and newer pages. Verify messages remain in chronological order, consecutive actions stay grouped, and loading a page does not move the visible message.
  3. Open links to older, newest, and unread messages, including a target at the beginning of the loaded data. Verify the target appears in the correct position, the unread marker stays with the correct message, and the chat start message never overlaps visible content while the list is measuring.
  4. Stay at the bottom while another account sends a message. Verify the list follows the new message.
  5. Scroll up into history while another account sends a message. Verify the list keeps the current reading position instead of jumping to the new message.
  6. Edit recent and older messages, and receive a streamed Concierge response. Verify edits and streamed text stay attached to the correct message.
  7. Send messages containing URLs, single-line code, code blocks, quotes, headings, bold, strikethrough, and italic text. Verify every format renders correctly before and after scrolling the row off-screen and back.
  8. Scroll repeatedly through text, image, and PDF rows. Verify recycled rows do not carry content, selection, preview, or loading state into another message.
  9. Open a direct chat link while signed in, then open the same link while signed out and sign in. Verify the requested chat opens and scrolling works in both flows.
  10. Open a short chat whose loaded actions do not fill the viewport but which has older history. Verify older pages load until the viewport can scroll or the oldest action is reached.
  11. Cause the initial report request to fail, then retry it. Verify the loading cover clears after the terminal failure and the report can load normally after the retry.
  • Verify that no errors appear in the JS console

Offline tests

  1. Open a chat online so its history is cached, disconnect the network, and reopen the chat. Verify the cached messages remain visible in chronological order and existing message grouping is unchanged.
  2. While offline, scroll to an unloaded edge of the cached history. Verify the visible messages stay in place and the list does not enter a broken or empty state.
  3. Reconnect while history is loading. Verify the cached page remains visible until hydration completes, the missing page loads, and the visible message does not jump.
  4. Disconnect during pagination, reconnect, and retry by scrolling to the same edge. Verify pagination recovers and the requested messages appear once without duplicates.

QA Steps

Same as Tests and Offline tests.

  • Verify that no errors appear in the JS console

PR Author Checklist

I reviewed every item for this PR. A checked conditional item means I verified it or determined that it does not apply. I attached the available native evidence below; Android screen recordings are still TBD, and web recordings are deferred. The checked recordings item records that review and the available evidence, not that every platform has a video.

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

iOS: Native

Cold chat open and initial loading

FlashList is on the left and LegendList is on the right. FlashList leaves the report body blank during loading; LegendList shows the report-action skeleton before messages appear.

LegendListLoadingSkeletonVsFlashList.mp4

Cold expense-chat open

FlashList is on the left and LegendList is on the right. Both progress from the loading skeleton to an expense preview and its surrounding report actions.

ExpenseChatCold.mp4

Warm expense-chat open

FlashList is on the left and LegendList is on the right. Both open the cached expense preview and report actions from the inbox.

ExpenseChatWarm.mp4

Warm chat open

Side-by-side opens of the same cached chat, ending on the same report-action history. The implementations are not labeled in this recording.

LegendListWarmReportOpen.mp4
Android: Native

Cold chat open and initial loading

FlashList is on the left and LegendList is on the right. FlashList leaves the report body blank during loading; LegendList shows the report-action skeleton before messages appear.

ColdOpenLoading.mp4

Cold expense-chat open

FlashList is on the left and LegendList is on the right. Both progress from the loading skeleton to an expense preview and its surrounding report actions.

Android screen recording TBD.

Warm expense-chat open

FlashList is on the left and LegendList is on the right. Both open the cached expense preview and report actions from the inbox.

Android screen recording TBD.

Warm chat open

Side-by-side opens of the same cached chat, ending on the same report-action history. The implementations are not labeled in this recording.

Android screen recording TBD.

Web recordings are deferred.

@chrispader
chrispader force-pushed the @chrispader/feat/legend-list-report-actions branch from 1bac411 to c067b0c Compare September 9, 2026 18:27
@chrispader
chrispader force-pushed the @chrispader/feat/legend-list-report-actions branch from c067b0c to e784ebe Compare September 9, 2026 20:24
@chrispader chrispader changed the title feat: migrate report actions to chronological LegendList [LegendList 2/5] feat: migrate report actions to chronological LegendList Sep 9, 2026
@chrispader
chrispader marked this pull request as ready for review September 10, 2026 09:44
@chrispader
chrispader requested review from a team as code owners September 10, 2026 09:44
@melvin-bot
melvin-bot Bot requested review from TaduJR and garrettmknight and removed request for a team and TaduJR September 10, 2026 09:44
@melvin-bot

melvin-bot Bot commented Sep 10, 2026

Copy link
Copy Markdown

@TaduJR Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot
melvin-bot Bot requested review from a team and removed request for a team September 10, 2026 09:44
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-10T10:07:15.590070Z f616302 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@codecov

codecov Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ Changes either increased or maintained existing code coverage, great job!

Files with missing lines Coverage Δ
src/components/FlashList/index.tsx 100.00% <100.00%> (ø)
src/components/FlatList/FlatList/index.ios.tsx 90.47% <100.00%> (ø)
src/components/FlatList/FlatList/index.tsx 50.98% <100.00%> (+50.98%) ⬆️
...c/components/KeyboardDismissibleFlatList/index.tsx 100.00% <100.00%> (+100.00%) ⬆️
...mponents/LegendList/allowLegendListItemOverflow.ts 100.00% <100.00%> (ø)
...equestReportView/MoneyRequestReportActionsList.tsx 97.14% <100.00%> (+0.17%) ⬆️
...stReportView/MoneyRequestReportTransactionList.tsx 75.00% <ø> (ø)
...equestReportView/MoneyRequestReportUnifiedList.tsx 12.63% <ø> (ø)
...eyRequestReportView/useMoneyRequestReportScroll.ts 96.34% <ø> (ø)
...nents/ReportActionItem/ReceiptPDFOverlay/index.tsx 90.19% <100.00%> (ø)
... and 28 more
... and 116 files with indirect coverage changes

Comment thread src/pages/inbox/report/ReportActionsList.tsx Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f616302cdb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/pages/inbox/report/ReportActionsList.tsx
Comment thread src/pages/inbox/report/ReportActionCompose/useEditMessage.ts Outdated
@chrispader chrispader changed the title [LegendList 2/5] feat: migrate report actions to chronological LegendList [HOLD on #100732][LegendList 2/5] feat: migrate report actions to chronological LegendList Sep 10, 2026
@chrispader
chrispader force-pushed the @chrispader/feat/legend-list-report-actions branch from f616302 to 2a3323a Compare September 10, 2026 11:11
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ This PR is possibly changing native code and/or updating libraries, it may cause problems with HybridApp. Please check if any patch updates are required in the HybridApp repo and run an AdHoc build to verify that HybridApp will not break. Ask Contributor Plus for help if you are not sure how to handle this. ⚠️

@chrispader chrispader changed the title [HOLD on #100732][LegendList 2/5] feat: migrate report actions to chronological LegendList [HOLD on #100732] [LegendList 2/5] feat: migrate report actions to chronological LegendList Sep 10, 2026
chrispader and others added 5 commits October 22, 2026 13:37
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
chrispader and others added 2 commits October 22, 2026 14:16
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
@dmkt9

dmkt9 commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

@chrispader We have conflicts

Co-authored-by: Codex <noreply@openai.com>

Copy link
Copy Markdown
Contributor Author

Conflicts resolved

chrispader and others added 2 commits October 24, 2026 20:21
Co-authored-by: Codex <noreply@openai.com>
Keep 500 cached actions while measuring a 50-action displayed page. The LegendList Jest mock eagerly renders every supplied row, which exhausted the performance runner heap with the unbounded fixture. Verify that edits update a displayed row and start renderer-specific baselines for the new fixture.

Co-authored-by: Codex <noreply@openai.com>
@dmkt9

dmkt9 commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor
Fixed ✅

* The report always shows the loading state whenever it's opened. The same happens when opening a thread and returning to the parent chat.
* Android: The submitted expense isn't positioned correctly within the viewport when the report is opened. The highlighted comment also briefly flashes when returning from a thread to the parent chat.
* iOS: When the highlighted comment is the last item, it isn't displayed within the viewport.

Details
bug1.mp4
---

Sometimes, the last item being edited isn't scrolled into the viewport:

Details https://github.com/user-attachments/assets/0c461e30-fcc8-450a-847b-29a837e2115d

Clicking the Show History button in the Concierge chat scrolls to the beginning of the report. Additionally, if it doesn't scroll to the beginning of the report, the scroll position jumps slightly.

Details **Steps to reproduce:**
  1. Refresh the app while in the Concierge chat.
  2. Open a chat with an active need to do.
  3. Go back to the Concierge chat.
  4. Click the Show History button.
bug4.mp4

On android, the entire list flashes:

Details
bug1.mp4

The list jumps back to the bottom:

Similar to a previously reported bug: #100733 (comment). This time, the bug is more noticeable.

Steps to reproduce:

1. Delete the latest item.
2. Once the latest item disappears, scroll up.

Details
bug2.mp4

The hover style flickers on items when scrolling with the mouse:

Details
bug3.mp4

The highlighted item position isn't reliable when opening the chat:

Details
bug4.mp4

Similarly, on web, the highlighted item is out of the viewport when it's a PDF preview:

Details
bug71.mp4

The loading state appears after sending a link message:

Details
bug8.mp4

The keyboard appears briefly:

Details
bug5.mp4

The keyboard doesn't appear when editing a long comment:

Details
bug6.mp4

On Android, the highlighted item is out of the viewport if there is a long comment above it:

Details
bug7.mp4

Unable to test due to a new bug:

The app automatically navigates back to the Home screen when "Reply in thread".

Details
bug2.mp4

Android: The highlighted item scrolls down and then back up:

bug1.mp4

Android: Scrolling slightly right after the loading state disappears causes the list to jump:

bug3.mp4

Web: The "Submit" badge needs to be clicked multiple times (sometimes 3–4 times) to scroll to the highlighted item.

Sometimes, the top of the highlighted item or the bottom of the last item in the list is obscured.

bug4.mp4

Bug: Automatically scroll to the highlighted item when opening the PDF viewer:

Details
bug1.mp4

Bug: In a DM chat, the initial viewport isn't scrolled to the bottom when there's a new message, especially when the chat contains an IOU.

Details
bug2.mp4

Web: The chat in a thread isn't inverted.

Details
bug3.mp4

Remaining ⚠️

N/A


New bug ❗

Bug 1: The loading skeleton disappears when opening a report in offline mode.

Details
bug1.mp4

Bug 2: The highlighted item isn't highlighted or rendered within the initial viewport when returning to the parent report from a thread.

Details
bug2.mp4

Bug 3: Scrolling to the bottom using the Latest Message badge causes the loading skeleton to appear, and the viewport ends up above the bottom of the chat.

Details
bug3.mp4

Bug 4: The list anchor isn't preserved when loading older messages, causing the current viewport to jump.

Details
bug4.mp4

Bug 5: The list automatically scrolls to the bottom when opening a report marked as unread.

Details
bug5.mp4

Bug 6: The composer is hidden behind the keyboard when there's a draft message.

The same issue occurs if we focus the composer while the report is loading. Additionally, if we send a message while the report is loading, the list doesn't scroll to the bottom once the loading state ends.

Details
bug6.mp4

NAB: Can we make sure the unread indicator is fully visible when using the "New message" badge?

Details
nab1.mp4

Bug 7: The "New message" badge flashes briefly.

Details
bug7.mp4


// Report actions are stored newest-first. LegendList intentionally has no inverted mode, so
// give it chronological data and use its normal start/end and scrolling semantics.
const listData = reportActionsToRender.toReversed();

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.

I just realized that using toReversed could cause compatibility issues with older browser versions. We also don't use toReversed anywhere else in our app, and we don't have a polyfill for it.

@chrispader

chrispader commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks @dmkt9.

Since the scope of this PR is already pretty big (70+ files) and there are more and more bugs coming in every review round and fixing one set of bugs seems to re-introduce others, i'll split this PR up into three smaller PRs:

  1. Report lifecycle and editing (~18 files)
    Focus-ref resolution, notification-banner sizing, link guard, edit scrolling and local versus realtime notifications. Keep inverted scrolling. Can be reviewed and merged independently.
  2. Shared refs and row identity (~18 files)
    Ref contracts, adapters, attachment identity and state resets. Keep FlashList; use ordinary state until the migration. Can be reviewed and merged independently.
  3. Chronological LegendList migration (~41 files)
    Renderer, ordering, pagination, unread and badge geometry, end following, loading cover, telemetry, native patches and migration tests. PRs 1&2 are prerequisites for this PR.

I've also analyzed whether some of the changes can be slimmed down even further. I will check that and do some more testing before i hand it over to @dmkt9 and @roryabraham 🙌🏼


Update:

The split is now in layers 4 through 6 of the nine-part rollout:

  1. #104977: composer focus and scroll notifications, 18 files.
  2. #104978: shared list refs and recycled row state, 20 files.
  3. #100733: chronological LegendList migration, 34 files in the isolated migration diff.

Layers 4 and 5 are independent and share no changed files. Merge both before layer 6, then sync main into #100733 to remove their work from its Files changed tab. The only file shared with the migration is the temporary FlashList type export, which layer 5 keeps for compatibility and layer 6 removes.

The full review and merge order is tracked in #98994.

Resolve the current composer after delayed focus, reserve notification-banner space on initial layout, and keep inverted-list scroll events scoped to their existing callers. Expose local and realtime notification sources with a backward-compatible callback signature.

Co-authored-by: Codex <noreply@openai.com>
@dmkt9

dmkt9 commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Yep. This PR has quite a broad impact. I asked Codex to generate a testing checklist, and it came up with a lot of test cases.

## Manual Testing Checklist

**Priority levels:**
- **P0:** Critical flows that must be tested in this PR.
- **P1:** Important regression tests.
- **P2:** Additional checks.

### A. Initial Loading, Hydration, and Navigation

| ID | Test Steps | Expected Result | Priority |
|---|---|---|---|
| A01 | Open a DM for the first time with no cache. | The skeleton disappears once the list is ready, and the latest messages are displayed. | P0 |
| A02 | Open a cached DM and refresh the page. | The list does not jump to older messages before scrolling to the bottom. | P0 |
| A03 | Throttle the network, open a cached DM, and delay `OpenReport`. | No blank spaces or incorrectly ordered messages appear during hydration. | P0 |
| A04 | Open a chat containing only 1–2 messages. | Content is positioned correctly, with no unexpected scrollable area. | P0 |
| A05 | Open a chat containing hundreds of messages with varying heights. | No layout jumps occur while row dimensions are being measured. | P0 |
| A06 | Switch repeatedly between DM A → DM B → DM A. | Scroll positions and messages do not get mixed up between reports. | P0 |
| A07 | Open a chat, open the RHP, then close it. | The chat viewport is not unexpectedly reset. | P1 |
| A08 | Open a report in a narrow layout, then switch to a wide layout. | The initial scroll position remains correct after the layout changes. | P1 |
| A09 | Open a chat from a search result or notification. | Navigation opens the correct report at the requested position. | P1 |
| A10 | Open a chat when `OpenReport` fails or is interrupted. | The skeleton does not remain indefinitely, and available content remains usable. | P1 |

Pay particular attention to A02–A05 on Android, as the two LegendList patches added in this PR are intended to address viewport jumps caused by changes in content dimensions.

### B. Deep Links to Messages — `useLinkedReportActionScroll`

This section directly relates to the file reviewed.

| ID | Test Steps | Expected Result | Priority |
|---|---|---|---|
| B01 | Copy a link to a message in the middle of a chat and open it. | The message is positioned correctly and highlighted. | P0 |
| B02 | Open a link to the latest message. | The entire message is visible and is not obscured by the footer or composer. | P0 |
| B03 | Open a link to the first or a very old message. | Older messages are loaded if necessary, and the correct message is located. | P0 |
| B04 | Open a link to a message containing a slowly loading PDF or image. | The message remains at the intended position after the preview changes height. | P0 |
| B05 | Open a link, then manually scroll using the mouse or touch. | Automatic positioning stops and does not pull the user back to the linked message. | P0 |
| B06 | On web, scroll using the mouse wheel, trackpad, ArrowUp/Down, PageUp/Down, Home/End, or Space. | The linked-message positioning mechanism does not override user scrolling. | P1 |
| B07 | Open a link → open an attachment or thread → return. | The user's current viewport is preserved instead of jumping back to the previously linked message. | P0 |
| B08 | Send a new message while viewing a linked message. | The list switches to live-tail behavior at the appropriate time and stops pulling back to the linked message. | P0 |
| B09 | Open two different message links within the same report. | Each link correctly navigates to its respective message. | P0 |
| B10 | Reopen the same link after returning to the latest messages. | The linked message is positioned correctly again. | P1 |
| B11 | Delete the linked message or open a link to a deleted message. | The app does not crash, and navigation recovers appropriately. | P1 |
| B12 | Open a linked report in the background, then focus it. | Scrolling occurs only when the screen is actually focused. | P1 |

**Important edge case:** Combine B04 and B05. Open a deep link to a PDF, manually scroll away before the PDF finishes loading, and wait for the PDF preview to change height. The list must not pull the user back to the linked message.

### C. Bidirectional Pagination and Scroll Position Preservation

| ID | Test Steps | Expected Result | Priority |
|---|---|---|---|
| C01 | Scroll to the beginning of a long chat history. | `GetOlderActions` is called, and older messages are loaded. | P0 |
| C02 | Repeatedly scroll near the beginning while a pagination request is pending. | No duplicate requests are sent for the same cursor. | P0 |
| C03 | Load older messages containing images or PDFs that change height. | The message currently being read does not jump. | P0 |
| C04 | Open a report in the middle of its history, then scroll to the bottom. | Newer actions are loaded in the correct order. | P0 |
| C05 | Scroll from older messages back to the latest messages. | No duplicated or missing actions appear. | P0 |
| C06 | While reading older messages, have another user send a new message. | The list does not automatically jump to the bottom while the user is reading older content. | P0 |
| C07 | While at the bottom, have another user send a new message. | The list automatically follows the new message according to the existing behavior. | P0 |
| C08 | Stay near the bottom within the autoscroll threshold, then receive a new message. | The list follows the new message to the bottom instead of remaining at the previous position. | P1 |
| C09 | Simulate a `GetOlderActions` failure, restore the network, and retry loading. | Retrying is possible, and pagination is not blocked by the previous cursor. | P0 |
| C10 | Go offline while reading older messages, then reconnect. | The scroll position is preserved, and messages are not duplicated. | P1 |
| C11 | Open a list shorter than the viewport height that still has older actions. | Older actions can still be loaded through pagination. | P1 |
| C12 | Switch reports while a pagination request is still pending. | The previous report's request does not affect the new report's scroll position. | P1 |


### D. New Messages, Optimistic Updates, and Live-Tail Behavior

| ID | Test Steps | Expected Result | Priority |
|---|---|---|---|
| D01 | Send a text message in a DM while at the bottom. | The optimistic message appears, and the viewport follows it to the bottom. | P0 |
| D02 | Send multiple messages consecutively. | No incorrect scrolling or duplicated messages occur. | P0 |
| D03 | Send a message while reading older messages. | Follow/no-follow behavior remains consistent with the app's existing policy. | P0 |
| D04 | Open the same chat with two accounts and send a message from account B. | Account A receives the message in real time and displays/scrolls correctly. | P0 |
| D05 | Send a message offline, then reconnect. | The optimistic message is preserved and does not disappear during synchronization. | P0 |
| D06 | Send a message from a historical page that does not contain the newest actions. | The latest page is loaded, the list scrolls to the bottom, and pagination is pruned correctly. | P0 |
| D07 | Send a message while `OpenReport` is pending. | The optimistic message is not missed or stuck behind the loading state. | P0 |
| D08 | Edit or delete a message from another device. | The real-time update does not cause an unexpected scroll to the bottom. | P1 |
| D09 | Send a money request or an action without a report-action payload. | Live-tail behavior continues to work correctly. | P1 |
| D10 | Send an action, then quickly switch to another report. | Callbacks from the previous report do not scroll the new report to the bottom. | P1 |
| D11 | Send a message whose optimistic action fails to render or encounters an error. | The list does not unexpectedly scroll several seconds later when the action reappears. | P1 |
| D12 | Have Concierge or a custom agent stream a long response. | The draft grows smoothly without repeatedly jumping or resetting the viewport. | P0 |


### E. Unread Markers, Latest Messages, and Action Badges

| ID | Test Steps | Expected Result | Priority |
|---|---|---|---|
| E01 | Mark a message in the middle of the chat history as unread, close the chat, and reopen it. | The chat opens at the unread marker instead of incorrectly jumping to the beginning or bottom. | P0 |
| E02 | Scroll past the unread marker. | The read state updates correctly, and the marker/counter disappears at the appropriate time. | P0 |
| E03 | Scroll far up in a fully read chat. | The Latest messages pill appears after crossing the threshold. | P0 |
| E04 | Click Latest messages. | The list scrolls to the newest action and updates the read status. | P0 |
| E05 | Open a chat containing unread actions when the latest page has not been loaded. | Clicking the pill loads the newest actions and scrolls to the bottom. | P0 |
| E06 | Expand the composer to fullscreen, scroll or click the pill, then collapse it. | The pill is displayed correctly based on the current scroll position. | P1 |
| E07 | Create an action badge through an actionable preview, then click the badge. | The list scrolls to the correct actionable item. | P0 |
| E08 | Complete the first actionable item while another remains. | The badge can follow the next actionable item without jumping backward. | P1 |
| E09 | Load older actions through pagination while an action badge is present. | The list does not automatically scroll to the badge simply because its index changes. | P1 |
| E10 | Position the badge target above the viewport. | The badge is displayed correctly and is not hidden due to reversed indices. | P1 |

### F. Message Actions and Cell Recycling

Prepare a long chat containing different message types, including text, attachments, emoji reactions, expense previews, links, and payment actions.

| ID | Test Steps | Expected Result | Priority |
|---|---|---|---|
| F01 | Open the context menu using right-click or long-press. | The entire menu is visible and is not clipped by the row. | P0 |
| F02 | Open and close the menu, scroll far away, then return. | The menu does not reopen automatically or appear on another message. | P0 |
| F03 | Open the emoji picker and add/remove a reaction. | The picker and reaction are associated with the correct message. | P0 |
| F04 | Open the reaction list, then scroll quickly. | The reaction list state does not become associated with another recycled cell. | P1 |
| F05 | Open the payment method popover for an actionable item. | The popover appears correctly and does not leak into another row. | P1 |
| F06 | Edit an older message outside the viewport. | The list scrolls to the correct message for editing. | P0 |
| F07 | Edit the latest message and save the changes. | The editor closes, and the message remains fully visible at the correct scroll position. | P0 |
| F08 | Edit an older message, save, and continue reading. | The viewport does not unexpectedly jump to the bottom. | P1 |
| F09 | Delete or hide a message, then scroll. | No stale content appears in recycled cells, and no persistent blank spaces remain. | P1 |
| F10 | Scroll rapidly through hundreds of short and long messages. | No incorrect content, stale menu state, or missing messages appear. | P0 |
| F11 | Switch between messages containing previews or attachments. | Each row displays the correct preview with the correct dimensions. | P0 |
| F12 | Open a context menu near the top or bottom edge of the viewport. | The menu is not clipped and remains fully interactive. | P1 |

### G. Images, PDFs, and Dynamic Layout Changes

| ID | Test Steps | Expected Result | Priority |
|---|---|---|---|
| G01 | Send landscape and portrait images in a chat. | Image aspect ratios and heights are correct. | P0 |
| G02 | Open a chat containing images that have not finished loading. | No unexpected viewport jumps occur when image dimensions are measured. | P0 |
| G03 | Upload or open a PDF receipt in an expense/chat. | Loading, preview, and hover zoom work correctly. | P0 |
| G04 | Open a very long or multi-page PDF. | The preview does not become blank or crash, and the viewport remains correctly positioned. | P1 |
| G05 | Simulate a PDF loading failure, then open another PDF. | The error state does not leak into the new PDF. | P0 |
| G06 | Open multiple attachments consecutively while the list recycles cells. | Images or PDFs from previous messages do not appear in other messages. | P0 |
| G07 | Replace an expense receipt, then return to the chat. | The thumbnail updates to the new URL without retaining stale dimensions. | P1 |
| G08 | Change the height of a PDF preview or image above a linked message. | The linked target maintains its position unless the user has manually scrolled away. | P0 |

### H. Expense Reports, Transaction Threads, and Invoices

This section should not be skipped simply because most changes are in `ReportActionsList`.

| ID | Test Steps | Expected Result | Priority |
|---|---|---|---|
| H01 | Open an expense report containing only one expense. | Content starts at the correct position without scrolling incorrectly to the bottom. | P0 |
| H02 | Open an expense report containing multiple expenses. | The unified list displays all transactions and report actions. | P0 |
| H03 | Open a transaction thread and read its comments. | Scrolling and divider lines are correct. | P0 |
| H04 | Click X Replies from an expense/report preview. | The thread opens at the latest reply instead of the beginning of the report. | P0 |
| H05 | Edit the latest comment in a multi-expense report. | The list waits for the FlashList commit before scrolling so that the message is fully visible. | P0 |
| H06 | Edit an older comment in a multi-expense report. | No incorrect viewport jumps occur. | P1 |
| H07 | Reject, hold, or approve an expense from a report. | New actions and actionable previews update correctly. | P1 |
| H08 | Create a new money request, then navigate to its report. | The content does not jump when the server replaces the optimistic CREATED action. | P1 |
| H09 | Open an invoice report and a task thread. | Initial top alignment, dividers, and scrolling are correct. | P1 |
| H10 | Send another comment while a Concierge thinking indicator is displayed in an expense report. | The footer remains correctly positioned, and scrolling does not obscure the indicator. | P1 |

### I. Concierge and Special Report Types

| ID | Test Steps | Expected Result | Priority |
|---|---|---|---|
| I01 | Open Concierge from the main DM when older history is hidden. | Only actions belonging to the current session are displayed. | P0 |
| I02 | Click Show previous messages / Show history. | History appears correctly, and the scroll position is preserved if the user was at the bottom. | P0 |
| I03 | Click Show history, then scroll up before the request completes. | The list does not pull the user back to the bottom after loading finishes. | P0 |
| I04 | Open Concierge in a side panel. | The welcome state and chat content are displayed correctly. | P1 |
| I05 | Have Concierge stream a response and finish streaming. | The draft transitions into a persisted message without duplication. | P0 |
| I06 | Finish a Concierge response and display the feedback prompt. | The prompt appears next to the correct response. | P1 |
| I07 | Open the feedback prompt and submit thumbs down. | No incorrect prompt appears in the feedback thread. | P1 |
| I08 | Open a report containing a merchant rule suggestion banner. | The banner does not scroll incorrectly or obscure content. | P1 |
| I09 | Test web notification permissions with `default`, `granted`, and `denied`. | The banner is displayed correctly without causing an initial layout jump. | P1 |
| I10 | Open a resolved or reopened support ticket. | Survey actions are filtered and displayed correctly. | P2 |

### J. Composer, Keyboard, and Shared Lists

| ID | Test Steps | Expected Result | Priority |
|---|---|---|---|
| J01 | Focus the composer and open the keyboard on iOS/Android. | The latest message remains visible and is not obscured by the keyboard. | P0 |
| J02 | Scroll the chat while the keyboard is open. | Keyboard dismissal and scrolling work correctly. | P0 |
| J03 | Open and close a modal/RHP, then focus the composer. | The current input receives focus without using a stale ref. | P1 |
| J04 | Start editing a message and switch focus between the composer and editor. | Focus does not move to the wrong input, and the caret does not jump. | P1 |
| J05 | Expand the composer to full size, then collapse it. | The chat list and Latest messages pill remain synchronized. | P1 |
| J06 | On web, scroll the chat while composer suggestions/autocomplete are open. | Scroll events do not break suggestions or focus. | P1 |
| J07 | Scroll another screen that still uses a non-inverted FlashList/FlatList. | No unexpected composer scroll events occur. | P2 |
| J08 | Scroll an inverted list that still exists on another screen. | Existing scroll event behavior continues to work correctly. | P2 |

### K. Android Native Scroll Anchoring

This section requires testing on a physical Android device or emulator. Unit tests alone are insufficient to validate Fabric/native scroll clamping behavior.

| ID | Test Steps | Expected Result | Priority |
|---|---|---|---|
| K01 | Open a long chat at the newest messages. | Older messages do not briefly flash before the list returns to the bottom. | P0 |
| K02 | Open a chat at an unread marker near the bottom. | The exact position is preserved after row dimensions are measured. | P0 |
| K03 | Scroll to the middle of a chat and load older actions. | No large scroll jumps occur. | P0 |
| K04 | While at the bottom, increase the height of the latest message or preview. | The viewport correctly follows the latest message. | P0 |
| K05 | While reading the middle of a chat, change the heights of multiple images/PDFs above the viewport. | The content being read remains stable. | P0 |
| K06 | Open a deep link to a PDF in the middle of a chat. | No double compensation occurs that causes excessive scrolling. | P0 |
| K07 | Test another FlatList/ScrollView using `maintainVisibleContentPosition`. | No regressions occur due to the globally applied React Native patch. | P1 |
| K08 | Change the viewport height by opening the keyboard, rotating the device, or resizing the window. | No incorrect scroll clamping occurs. | P1 |

Share the report list imperative contract while retaining a compatibility export for inverted FlashList. Keep row-local preview state tied to its action and let money-report edit scroll requests survive until the list commits its layout.

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
@chrispader chrispader changed the title [LegendList 4/7] feat: migrate report actions to chronological LegendList [WIP] [HOLD on #104977, #104978] [LegendList 6/9] feat: migrate report actions to chronological LegendList Oct 26, 2026
chrispader and others added 4 commits October 26, 2026 09:10
Merge the independently reviewable composer and row-state preparation branches without rewriting the existing migration history. The migration-only comparison is 35 files, with the legacy FlashList type bridge as the only shared path. Drop the unrelated telemetry cleanup and restore the existing iOS callback structure.

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
Infer the action subscriber callback type without exporting its event source.

Co-authored-by: Codex <noreply@openai.com>
Remove the upstreamed Fabric content-size patch and port the Android end-clamp patch to the new release.

Co-authored-by: Codex <noreply@openai.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants