[$250] Empty space is shown after appear after restarting
If you haven’t already, check out our contributing guidelines for onboarding and email [email protected] to request to join our Slack channel!
Version Number: 9.2.71-3 Reproducible in staging?: Yes Reproducible in production?: Yes If this was caught during regression testing, add the test name, ID and link from TestRail: Email or phone of affected tester (no customers): Logs: https://stackoverflow.com/c/expensify/questions/4856 Expensify/Expensify Issue URL: Issue reported by: @dukenv0307 Slack conversation (hyperlinked to channel name): #expensify_bugs
Action Performed:
- Open https://staging.new.expensify.com/
- Go to Account -> Troubleshoot
- Press
Clear cache and restartthen confirm - Go to LHN, select any chat
Expected Result:
The empty space isn't shown
Actual Result:
The empty space is shown briefly
Workaround:
Unknown
Platforms:
Select the officially supported platforms where the issue was reproduced:
- [ ] Android: App
- [ ] Android: mWeb Chrome
- [ ] iOS: App
- [ ] iOS: mWeb Safari
- [ ] iOS: mWeb Chrome
- [x] Windows: Chrome
- [ ] MacOS: Chrome / Safari
- [ ] MacOS: Desktop
Platforms Tested:
On which of our officially supported platforms was this issue tested:- [ ] Android: App
- [ ] Android: mWeb Chrome
- [ ] iOS: App
- [ ] iOS: mWeb Safari
- [ ] iOS: mWeb Chrome
- [x] Windows: Chrome
- [ ] MacOS: Chrome / Safari
- [ ] MacOS: Desktop
Screenshots/Videos
https://github.com/user-attachments/assets/e7c24d4d-f166-437a-b4fa-e32facddb28d
https://github.com/user-attachments/assets/f198f6c6-fb5e-425b-afdd-5109e1a24e5b
Upwork Automation - Do Not Edit
- Upwork Job URL: https://www.upwork.com/jobs/~021996580198908307475
- Upwork Job ID: 1996580198908307475
- Last Price Increase: 2025-12-25
Issue Owner
Current Issue Owner: @sobitneupane
Triggered auto assignment to @trjExpensify (Bug), see https://stackoverflow.com/c/expensify/questions/14418 for more details. Please add this bug to a GH project, as outlined in the SO.
Job added to Upwork: https://www.upwork.com/jobs/~021996580198908307475
Triggered auto assignment to Contributor-plus team member for initial proposal review - @sobitneupane (External)
@dukenv0307 It's more of a content shift for me:
https://github.com/user-attachments/assets/c1da0f71-540f-4fd3-a833-498f0af836ce
@trjExpensify I’m seeing this as a missing header rather than a content shift. After clearing cache, opening a chat loads the most recent message and fires openReport for the rest, but until that response returns the created action (avatar + “Say hello!” / “This chat is with +49…”) doesn’t render, leaving an empty gap.
If this seems an issue to you then let us know to submit proposals.
https://github.com/user-attachments/assets/57fdb774-be65-479c-a9ee-ae7bbb08713b
Proposal
Please re-state the problem that we are trying to solve in this issue.
Empty space is shown where there should be a welcome header after clearing cache and opening a report.
What is the root cause of that problem?
There are unnecessary constraints when deciding on whether to create an optimistic CREATED action, which is needed to show the header:
https://github.com/Expensify/App/blob/d34df44329db681f7bdd1a8114cc44c41df161fe/src/pages/home/report/ReportActionsView.tsx#L126
PS: These constraints were introduced in this PR. I think if we decide to move forward with this proposal, it would be a good idea to ask the PR author if there's any specific reason these were added so we don't reintroduce any bugs (I couldn't find any after removing the constraints and opening reports that would otherwise be constrained).
What changes do you think we should make in order to solve the problem?
Only check if the report doesn't already have a created action:
const shouldAddCreatedAction = !isCreatedAction(lastAction);
What alternative solutions did you explore? (Optional)
N/A
Result:
https://github.com/user-attachments/assets/9cb0340c-9350-4dea-af74-4d6bc7c84e3f
🚨 Edited by proposal-police: This proposal was edited at 2025-12-20 10:56:16 UTC.
Proposal
Please re-state the problem that we are trying to solve in this issue.
After Restarting and clearing the cache, empty space is shown when select any report/chat.
What is the root cause of that problem?
Empty space issue
The Root cause of this problem lies inside src/pages/home/report/ReportActionsList.tsx file.
When we Clear cache and restart the app, it removes all the data from cache. So when we select any chat then it actually calls the openReport function and tries to fetch the report actions from the Backend.
During the backend call for full list of report actions, the frontend only shows some recent/last report actions (maybe to make the chat feels fast). So during the fetching of full list of report actions, we have some recent report actions and at that moment these report actions are placed at the bottom of the chat due to the following code inside InvertedFlatList component in src/pages/home/report/ReportActionsList.tsx:
<InvertedFlatList
// ...Rest props
contentContainerStyle={[
styles.chatContentScrollView,
shouldScrollToEndAfterLayout ? styles.visibilityHidden : styles.visibilityVisible,
shouldFocusToTopOnMount ? styles.justifyContentEnd : undefined,
]}
// ...Rest props
/>
So due to styles.justifyContentEnd those recent report actions are placed at the bottom during the fetching of Report actions which causes the blank space with some sort of report actions.
In Short: When report actions are not fully loaded after open the chat, the chat layout uses justifyContentEnd, which pushes the few visible actions to the bottom, which leads to the entire top area empty.
UI jump wierd behaviour
This isn't a separate issue but a wierd behaviour attach to the Empty space issue which feels the Chat behaviour broken.
When we open a chat after clearing cache and restarting the app, along with the empty space, there are 1 or two messages (one of them is a last message) which appears immediately, and if we created an expense report in the chat then it's preview was available immediately, so after loading, the older messages get injected in between the preloaded messages or expense reports.
What changes do you think we should make in order to solve the problem?
We have reportMetadata?.hasOnceLoadedReportActions inside ReportActionsList which evaluates whether the report actions are loaded completely from the Backend or not.
Till the fetching of report actions, we need to add !reportMetadata?.hasOnceLoadedReportActions in shouldShowSkeleton which decides whether to show the skeleton loader or not. So that user will see a loading screen, with the last message, untill all report actions are being fetched from backend and cached completely to get faster report action on open the chat next time.
Here are the changes which we need to make:
In src/pages/home/report/ReportActionsList.tsx
We need to add !reportMetadata?.hasOnceLoadedReportActions along with isOffline inside shouldShowSkeleton
// Before
const shouldShowSkeleton = (isOffline || !reportMetadata?.hasOnceLoadedReportActions) && !sortedVisibleReportActions.some((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED);
// After
const shouldShowSkeleton = isOffline && !sortedVisibleReportActions.some((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED);
We also need to pass the last message or last report action to data prop in InvertedFlatList component when report actions are loading or shouldShowSkeleton is true.
It prevents the wierd UI jump where (after fetching) old comments/messages are being inserted in between the already existing preloaded messages which were there immediately (from cache).
// Before
<InvertedFlatList
// props...
data={sortedVisibleReportActions}
// props...
/>
// After
<InvertedFlatList
// props...
data={shouldShowSkeleton ? [sortedVisibleReportActions[0]] : sortedVisibleReportActions}
// props...
/>
What alternative solutions did you explore? (Optional)
Reminder: Please use plain English, be brief and avoid jargon. Feel free to use images, charts or pseudo-code if necessary. Do not post large multi-line diffs or write walls of text. Do not create PRs unless you have been hired for this job.
@trjExpensify, @sobitneupane, could you please check my commit. When you get a chance. thanks
PS: These constraints were introduced in https://github.com/Expensify/App/pull/72001. I think if we decide to move forward with this proposal, it would be a good idea to ask the PR author if there's any specific reason these were added so we don't reintroduce any bugs (I couldn't find any after removing the constraints and opening reports that would otherwise be constrained).
@VickyStash @mountiny @luacmartins for input! IMO, if we aren't going to show the "start of the chat" content, it should appear with the skeleton loading UI instead of blank. Ideally, we show it though given we are some comments?
@trjExpensify My proposal exactly add the Skeleton loading while fetching actions, it is the better UX for the end user if showing immediate messages/actions isn't the priority.
🚨 Edited by proposal-police: This proposal was edited at 2025-12-10 08:30:50 UTC.
Proposal
Please re-state the problem that we are trying to solve in this issue.
After clearing cache and restarting, opening any chat from the LHN briefly shows a large empty space at the top of the message list, and older comments “pop in” without any visual indication that they are still loading.
What is the root cause of that problem?
After a cache clear, OpenReport is still loading older actions while the UI renders only a small tail of recent actions from allReportActions. In ReportActionsView, we only add an optimistic CREATED action (which renders the header/welcome block) for money request, invoice, or some transaction-thread reports:
https://github.com/Expensify/App/blob/9e1110a4556fd663505f4f8051bd397e48e02593/src/pages/home/report/ReportActionsView.tsx#L124-L126
For a normal chat during initial load this condition is false, so no CREATED action is present and the header doesn’t render. At the same time, ReportActionsList uses justifyContentEnd with an inverted list, so the few loaded actions sit at the bottom and the top area (where the header should be) looks like empty space. When OpenReport finally returns, older comments appear without any loader or placeholder, which feels like a “jump”.
What changes do you think we should make in order to solve the problem?
I propose two small, related changes:
- Always show the header during the initial load window.
In
ReportActionsView, generalize the existing optimisticCREATEDlogic so it also covers the initial loading window for all reports: https://github.com/Expensify/App/blob/9e1110a4556fd663505f4f8051bd397e48e02593/src/pages/home/report/ReportActionsView.tsx#L124-L126
const lastAction = allReportActions?.at(-1);
+ const hasCreatedAction = isCreatedAction(lastAction);
const isInitiallyLoadingTransactionThread =
isReportTransactionThread && (!!isLoadingInitialReportActions || (allReportActions ?? [])?.length <= 1);
const shouldAddCreatedAction =
+ !hasCreatedAction &&
(isMoneyRequestReport(report) ||
isInvoiceReport(report) ||
isInitiallyLoadingTransactionThread ||
!!isLoadingInitialReportActions);
- Keeps existing behavior for:
- Money request reports (
isMoneyRequestReport(report)), - Invoice reports (
isInvoiceReport(report)), - Transaction-thread reports (
isInitiallyLoadingTransactionThread).
- Money request reports (
- New behavior: while
isLoadingInitialReportActionsis true, we also synthesize aCREATEDaction for regular chats if there isn’t one yet, so the header/welcome block is visible from the first frame. - Once
OpenReportcompletes and the realCREATEDarrives,hasCreatedActionbecomes true andshouldAddCreatedActionreturns to false, so we don’t double-inject.
- Show a skeleton just under the header, above the already-loaded messages, while older actions are still loading.
In
ReportActionsList, detect when we’re in the initial-load window and insert a smallReportActionsSkeletonViewdirectly under theCREATEDaction: Add initial-load flags (above therenderItem):
const createdActionIndex = useMemo(
() => sortedVisibleReportActions.findIndex((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED),
[sortedVisibleReportActions],
);
const shouldShowInitialLoadSkeleton =
!isOffline &&
!!reportMetadata?.isLoadingInitialReportActions &&
!reportMetadata?.hasOnceLoadedReportActions &&
createdActionIndex !== -1;
Inject the skeleton directly beneath the header action (inside renderItem):
https://github.com/Expensify/App/blob/3558a15a51415bc322abcef44d55c268ad93a381/src/pages/home/report/ReportActionsList.tsx#L689-L737
const renderItem = useCallback(
({item: reportAction, index}: ListRenderItemInfo<OnyxTypes.ReportAction>) => {
...
const shouldRenderInitialLoadSkeleton =
shouldShowInitialLoadSkeleton && reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED;
return (
<>
{shouldRenderInitialLoadSkeleton && (
<ReportActionsSkeletonView
shouldAnimate
possibleVisibleContentItems={1}
/>
)}
<ReportActionsListItemRenderer
...
/>
</>
);
},
[
...,
shouldShowInitialLoadSkeleton,
createdActionIndex,
],
);
Keep the footer skeleton only for the offline case: https://github.com/Expensify/App/blob/3558a15a51415bc322abcef44d55c268ad93a381/src/pages/home/report/ReportActionsList.tsx#L811-L819
const shouldShowOfflineSkeleton =
isOffline && !sortedVisibleReportActions.some((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED);
const listFooterComponent = useMemo(() => {
if (!shouldShowOfflineSkeleton) {
return;
}
return <ReportActionsSkeletonView shouldAnimate={!isOffline} />;
}, [isOffline, shouldShowOfflineSkeleton]);
This gives us the desired stack:
- Header (from the
CREATEDaction), - Skeleton placeholder indicating older messages are still loading,
- Already-loaded messages,
- Composer at the bottom.
We keep the existing offline skeleton in the footer as-is (only for the offline case), and avoid adding a full-screen loader that hides real messages.
What specific scenarios should we cover in automated tests to prevent reintroducing this issue in the future?
None
What alternative solutions did you explore? (Optional)
@trjExpensify My https://github.com/Expensify/App/issues/76666#issuecomment-3622428940 exactly add the Skeleton loading while fetching actions, it is the better UX for the end user if showing immediate messages/actions isn't the priority.
With this approach we:
- show full loading screen
| Before | After |
|---|---|
- show loading on expense open
| Before | After |
|---|---|
I'm not sure if it's expected behaviour.
^^ The header area feels better there, as it's immediately available. I don't like the jumping around feeling of comments appearing though, it's not clear something is still loading in the chat, and feels broken as the magically appear.
With this approach we:
show full loading screen
Before After
@VickyStash do we have to do full loading screen though? Why can't we just have the area above the "loaded" actions in skeleton to signal that report actions further back are still loading? 🤔
CC: @Expensify/design
@VickyStash If possible could you please give some context on why you added the && (isMoneyRequestReport(report) || isInvoiceReport(report) || isInitiallyLoadingTransactionThread); conditions?
I'm trying to figure out if there's any bug my proposal might've caused I'm not seeing. Thanks :)
https://github.com/Expensify/App/blob/d34df44329db681f7bdd1a8114cc44c41df161fe/src/pages/home/report/ReportActionsView.tsx#L126
@trjExpensify You were Right! This issue seems like a content shift rather than the empty space. If we consider the @LorenzoBloedow and @marufsharifi approaches then it is still shifting the content But with heading and subtitle.
End user may think like, it's a glitch of shifting the content from bottom to slight top after loading, so loading should be better instead of jumping. It's my suggestion, i could be wrong too.
Btw @VickyStash I'm still finding the best approach from the UX and the end user perspective, I'll update in my solution in proposal.
@trjExpensify @suhailpthaj @VickyStash I see this a bit differently: it’s less a content shift and more that the header is blocked on openReport. The body comments render, but the header waits for the full load. From a UX point of view, I think the header (avatar, title, welcome) should be visible as soon as we know the report, even while older actions are still loading.
I’ve been testing a middle-ground between the full-loading approach and shouldAddCreatedAction = !isCreatedAction(lastAction).
Instead of adding a full loader or always injecting CREATED, I’d like to reuse the existing optimistic CREATED logic, but extend it only to the initial loading window for all reports:
const lastAction = allReportActions?.at(-1);
+ const hasCreatedAction = isCreatedAction(lastAction);
const isInitiallyLoadingTransactionThread =
isReportTransactionThread && (!!isLoadingInitialReportActions || (allReportActions ?? [])?.length <= 1);
const shouldAddCreatedAction =
+ !hasCreatedAction &&
(isMoneyRequestReport(report) ||
isInvoiceReport(report) ||
isInitiallyLoadingTransactionThread ||
!!isLoadingInitialReportActions);
This keeps existing behavior for IOU/invoice/transaction-thread reports, but ensures the header is visible from the first frame in regular chats too. We still stream older actions upward as they load, and we avoid:
- a full-screen loader hiding real messages, and
- a larger behavior change that might impact the IOU/expense/iOS scroll fixes.
it's not clear something is still loading in the chat, and feels broken as the magically appear Why can't we just have the area above the "loaded" actions in skeleton to signal that report actions further back are still loading? 🤔
Agree with Tom's comments here and am wondering the same.
@sobitneupane Huh... This is 4 days overdue. Who can take care of this?
Why can't we just have the area above the "loaded" actions in skeleton to signal that report actions further back are still loading? 🤔
I agree with Tom and Danny. It would be better if we could show a skeleton place holder above the loaded actions.
@trjExpensify @sobitneupane I’ve updated my proposal based on your feedback about the header and loading states.
- The header (CREATED + avatar/welcome) is now always visible from the first frame.
- Cached messages are still shown immediately (no full-screen loader).
- A
ReportActionsSkeletonView(loader) is rendered directly under the header and above the already-loaded messages while older actions are still loading.
Here’s a short demo of the current proposal. When you have a moment, could you please take a look and let me know if this approach looks good?
https://github.com/user-attachments/assets/1a971349-dcb0-4c0a-94a5-3fc0df82f5d5
@sobitneupane I was the first one to mention skeleton loader approach.
Thanks for that! I think it's definitely better than the "blank space, no feedback" behaviour in the OP, but it's still a little jarring because the skeleton rows don't match the number of comments that will eventually load - so we get the content shift feeling still when it goes away and the comments appear. I don't suppose we have a way to better match those?
@Expensify/design what do you think? I think it's definitely better, maybe skeleton loading the whole main pain would actually be less jarring, but it's a trade-off with making the app feel slower.
@mountiny might have an opinion on this too!
Maybe a really quick animation would ease off the content shift? 🤔
My very professional (😆) Figma demo :
https://github.com/user-attachments/assets/f421d9ef-a6f9-4bd1-a86e-3d396e3e3f24
I'm not a designer so I have no idea if this suggestion is desirable at all though :P
what do you think? I think it's definitely better, maybe skeleton loading the whole main pain would actually be less jarring, but it's a trade-off with making the app feel slower.
Curious for other people's thoughts too, but I agree this is definitely better. But like you, now I'm kinda torn about the whole pane loader vs. this latest option.
Sorry if this was discussed before in here, but does this issue occur in a normal user flow? What are the reproduction steps? Because usually i dont really consider the clear cache option as some troublesome. Normal users would rarely do that and I dont think it is something we should optimise for
@trjExpensify thanks a lot for checking the latest version and for the detailed feedback.
I’d still prefer not to use a full pane loader, since it hides messages we already have and doesn’t match how most other screens behave (we generally show partial content as soon as it’s available).
I see three concrete options:
- Header + full-area skeleton between header and first message: keep the header and cached messages visible, but let the skeleton fill the whole empty space between the header and the first loaded comment so the “shift” is just skeleton → content in that same area.
- Header + first message: header is visible from the first frame, cached messages appear as they do now, and we accept a bit of content shift as the trade-off for keeping the UI simpler.
- Leave the original behavior unchanged – no header until
OpenReportfinishes (as in the current production behavior).
No strong feelings from me. Similarly to Danny's thoughts. I think the only solve for all this other stuff is to avoid skeleton loaders and use something more like a spinner, but I think for now as long at it indicates that something is happening, it's fine
@marufsharifi Would you be able to help us understand if there is a similar flow in the app, maybe right after signing in and opening some reports where the user could experience this?
📣 It's been a week! Do we have any satisfactory proposals yet? Do we need to adjust the bounty for this issue? 💸