Skip to content

[LegendList 9/12] feat: migrate report actions to chronological LegendList - #100733

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

chrispader wants to merge 126 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 9 of the twelve-part LegendList rollout. 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.1 includes the upstream Android end-clamp and follow-threshold fixes. This PR also patches three web fixes into LegendList that keep the messages on screen in place. They're open upstream as LegendApp/legend-list#596, #597, and #598. The React Native scroll-preservation patch remains with this migration.

Composer focus and notification preparation landed in layer 4, #104977. Shared list refs, row state, and money-report edit scrolling landed in layer 5, #104978. Layers 6 to 8 split out the FlashList module split, #105473, the LegendList 3.7.0 upgrade, #105474, and the unread-tracking tests, #105475.

The first time a chat opens online after the app starts, the skeleton stays over its cached messages until OpenReport finishes and the list is positioned, so estimated row positions never show. The warm open-report span ends when the positioned messages replace that cover.

Fixed Issues

$ #98994

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

Tests

All platforms means desktop web, mobile web, Android, and iOS.

Use a second test account in the same chat for the incoming-message cases.

Web, first open and return to a chat

  1. Reload the app.
  2. Open a chat. A single loading placeholder should give way to correctly positioned messages.
  3. Open a different chat.
  4. Return to the first chat. Its messages should settle without jumping to older rows.

Android and iOS apps, first open and return to a chat

  1. Close the app.
  2. Reopen the app.
  3. Open a chat. A single loading placeholder should give way to correctly positioned messages.
  4. Open a different chat.
  5. Return to the first chat. Its messages should settle without jumping to older rows.

All platforms, message history and incoming messages

  1. Open a chat with a long history shared with the second test account.
  2. Scroll upward until older messages load. The message you were reading should stay in place.
  3. Scroll to the newest message. Messages should remain in order without duplicates, and consecutive messages from the same sender should stay correctly grouped.
  4. Send a message from the second account. The new message should become visible automatically.
  5. Scroll upward until the button for returning to the latest messages appears.
  6. Send another message from the second account. The message you were reading should stay in place.
  7. Use the button to return to the latest messages. The new message should be visible.
  8. Open a different chat on the account being tested.
  9. Send another message to the shared chat from the second account.
  10. Open the shared chat on the account being tested. The unread marker should identify the new message.

All platforms, a link to an older message

  1. Open a long chat.
  2. Open an older message's action menu.
  3. Copy the message link.
  4. Open a different chat.
  5. Paste the link into the message box.
  6. Send the link.
  7. Open the sent link. The linked message should be visible in the correct chat.

All platforms, reopening a chat with a recent unread message

  1. Open a chat with a long history.
  2. Open the third-newest message's action menu.
  3. Choose Mark as unread.
  4. Return to the inbox.
  5. Open the chat again. The newest messages should appear without briefly showing older messages.

All platforms, edit the newest and an older message

  1. Open a chat containing an older message that you sent.
  2. Send a short message.
  3. Open that message's action menu.
  4. Choose Edit.
  5. Change the message text.
  6. Save the edit. The updated message should remain visible at the bottom.
  7. Scroll to an older message that you sent.
  8. Open its action menu.
  9. Choose Edit.
  10. Change the message text.
  11. Save the edit. The updated message should remain at its original place in the conversation.

All platforms, formatted messages and attachments after scrolling

  1. Open a chat with scrollable history.
  2. Send *bold* _italic_ ~strike~ https://example.com. The formatting should render correctly.
  3. Send an image.
  4. Send a PDF.
  5. Scroll the formatted message and attachments off-screen.
  6. Scroll back to them. The formatting should remain correct, and each preview should show the correct attachment.
  7. Open the image. The correct image should open.
  8. Close the image viewer.
  9. Open the PDF. The correct document should open.

Web, opening a chat URL after signing in

  1. Open a chat.
  2. Copy its URL from the browser address bar.
  3. Sign out of the app.
  4. Open the copied URL. The sign-in screen should appear.
  5. Sign in. The requested chat should open.

Android and iOS apps, keyboard and message-box resizing

  1. Open a chat.
  2. Tap the message box. The keyboard should open.
  3. Enter several lines of text. The newest messages should remain correctly positioned above the message box.
  4. Dismiss the keyboard. The list should keep its position.
  5. Tap the message box. The keyboard should reopen.
  6. Send the draft. The new message should be visible at the bottom.
  • Verify that no errors appear in the JS console

Offline tests

All platforms, cached chats offline and history after reconnecting

  1. Open a chat with a long history while online.
  2. Disconnect the internet.
  3. Return to the inbox.
  4. Reopen the chat. The cached messages should remain visible in the correct order.
  5. Scroll to the beginning of the available history. Existing messages should remain visible.
  6. Reconnect the internet.
  7. Scroll upward. Older messages should load without duplicates or a jump in the message you were reading.

QA Steps

Run the All platforms cases and the blocks for the platform being tested, including Offline tests.

  • Verify that no errors appear in the JS console

PR Author Checklist

  • 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

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.

Android: mWeb Chrome
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
iOS: mWeb Safari
MacOS: Chrome / Safari

@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

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
...mponents/LegendList/allowLegendListItemOverflow.ts 100.00% <100.00%> (ø)
...equestReportView/MoneyRequestReportActionsList.tsx 97.32% <100.00%> (ø)
...eyRequestReportView/useMoneyRequestReportScroll.ts 96.12% <100.00%> (ø)
src/hooks/useReportActionsListModel.ts 100.00% <100.00%> (ø)
src/hooks/useReportScrollManager/index.native.ts 100.00% <100.00%> (+7.40%) ⬆️
...ort/ReportActionsList/ReportActionsListContent.tsx 98.54% <100.00%> (+0.06%) ⬆️
src/pages/inbox/report/ReportActionsList/index.tsx 100.00% <100.00%> (ø)
...port/ReportActionsList/useReportActionsListData.ts 91.95% <100.00%> (+0.49%) ⬆️
...portActionsList/useReportActionsListRenderItem.tsx 95.83% <100.00%> (-0.06%) ⬇️
...ReportActionsList/useReportActionsListTelemetry.ts 100.00% <100.00%> (+25.00%) ⬆️
... and 9 more
... and 43 files with indirect coverage changes

@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".

@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
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chrispader chrispader changed the title [LegendList 6/9] feat: migrate report actions to chronological LegendList [LegendList 9/12] feat: migrate report actions to chronological LegendList Oct 30, 2026
chrispader and others added 2 commits October 30, 2026 12:38
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chrispader

Copy link
Copy Markdown
Contributor Author

@chrispader Could you confirm whether these bugs have been fixed? I just noticed that Bug 1 is still reproducible

@dmkt9 @roryabraham i will confirm all previous bugs again today, now that the PR is much slimmer.

chrispader and others added 6 commits November 2, 2026 14:24
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
Treat a cleared message link as absent when choosing the initial unread target.

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

chrispader commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

New bug ❗

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

✅ Fixed!

Screen.Recording.2026-11-03.at.18.50.16.mov

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

✅ Fixed! @dmkt9 please confirm on a physical Android device.

Screen_Recording_20261103_194822_Expensify.Debug.legendlist.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.

✅ Fixed!

Screen.Recording.2026-11-04.at.09.45.11.mov

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

✅ Fixed! Scrolling/pagination keeps the list anchored. The jumpiness on initial report opening is handled separately.

Screen.Recording.2026-11-04.at.20.24.45.mov

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

✅ Fixed!

Screen_Recording_20261104_215846_Expensify.Debug.legendlist.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.

This isn't caused by LegendList. It also happens with main's list code on the same build. The keyboard library resets its keyboard state from the JS thread when a screen mounts. When a chat opens with a draft, that reset reaches the UI thread after the keyboard has already opened, so the composer never gets its padding and stays hidden. I reused the change from the still-open upstream PR kirillzyusko/react-native-keyboard-controller#1685 as a patch. The composer now ends up above the keyboard for both the draft case and focusing while the chat loads.

Since it's a separate bug that main has too, i'll create a separate PR for this fix.

bug6-draft-after-fix.mp4
bug6-focus-and-send-while-loading-after-fix.mp4

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

✅ Fixed!

nab-new-messages-badge.mp4

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

✅ Fixed!

bug7-concierge-no-badge-flash.mp4

chrispader and others added 7 commits November 4, 2026 13:19
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…esize

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The keyboard avoiding view set its starting keyboard state from the JS thread on mount. That write reached the UI thread after the keyboard had already opened and reset the view to a closed keyboard, so it never added bottom padding. Apply the upstream fix from kirillzyusko/react-native-keyboard-controller#1685, which sets the starting state on the UI thread and skips it once a keyboard event arrived.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A chat opened at its unread marker can settle within 250 pixels of the end. The next scroll event then made the list follow the end, so the rows measured below the marker scrolled the New divider out of view a moment after opening. Lists that start at an unread or linked message now follow the end only once they reach it or the user drags near it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chrispader and others added 2 commits November 4, 2026 20:51
After a cache clear, OpenApp stores a few scattered actions for a chat, such as
thread parents and its last message, but no page. The chat opened at those old
messages and jumped once OpenReport loaded the newest page. While OpenReport is
pending for a chat without a loaded page, show the loading skeleton instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chrispader

Copy link
Copy Markdown
Contributor Author

Chris's AI understudy reporting in. 🤖

On web, the messages on screen could still move in three cases: when older messages loaded at the top, when the oldest loaded message lost its sender header because older messages from the same sender loaded, and when older messages arrived right after a fast scrollbar drag. All three come from how LegendList keeps visible items in place on web. This PR fixes them with patches to LegendList, and we opened the same fixes upstream:

  • LegendApp/legend-list#596 applies a pending size correction in the next layout pass, so loading older messages no longer makes the visible ones jump and snap back.
  • LegendApp/legend-list#597 makes lists with alignItemsAtEnd keep the last visible item in place, like an inverted list does. A row that resizes above the reader then only moves the content above it.
  • LegendApp/legend-list#598 keeps new data anchored when it arrives right after a scroll jump.

We can remove the patches once a LegendList release includes these fixes.

@chrispader

Copy link
Copy Markdown
Contributor Author

@dmkt9 i've addressed all your issues, merged main and resolved conflicts. I'm checking right now whether there is anything else that needs fine-tuning, but feel free to already give this another review! 🙌🏼

…nd-list-report-actions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.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