App icon indicating copy to clipboard operation
App copied to clipboard

[$250] Empty space is shown after appear after restarting

Open izarutskaya opened this issue 1 month ago • 51 comments

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:

  1. Open https://staging.new.expensify.com/
  2. Go to Account -> Troubleshoot
  3. Press Clear cache and restart then confirm
  4. 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

View all open jobs on GitHub

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 OwnerCurrent Issue Owner: @sobitneupane

izarutskaya avatar Dec 03 '25 21:12 izarutskaya

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.

melvin-bot[bot] avatar Dec 03 '25 21:12 melvin-bot[bot]

Job added to Upwork: https://www.upwork.com/jobs/~021996580198908307475

melvin-bot[bot] avatar Dec 04 '25 13:12 melvin-bot[bot]

Triggered auto assignment to Contributor-plus team member for initial proposal review - @sobitneupane (External)

melvin-bot[bot] avatar Dec 04 '25 13:12 melvin-bot[bot]

@dukenv0307 It's more of a content shift for me:

https://github.com/user-attachments/assets/c1da0f71-540f-4fd3-a833-498f0af836ce

trjExpensify avatar Dec 04 '25 14:12 trjExpensify

@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

marufsharifi avatar Dec 07 '25 06:12 marufsharifi

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

LorenzoBloedow avatar Dec 07 '25 14:12 LorenzoBloedow

🚨 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.

suhailpthaj avatar Dec 07 '25 16:12 suhailpthaj

@trjExpensify, @sobitneupane, could you please check my commit. When you get a chance. thanks

marufsharifi avatar Dec 08 '25 05:12 marufsharifi

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 avatar Dec 08 '25 11:12 trjExpensify

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

suhailpthaj avatar Dec 08 '25 11:12 suhailpthaj

🚨 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:

  1. Always show the header during the initial load window. In ReportActionsView, generalize the existing optimistic CREATED logic 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).
  • New behavior: while isLoadingInitialReportActions is true, we also synthesize a CREATED action for regular chats if there isn’t one yet, so the header/welcome block is visible from the first frame.
  • Once OpenReport completes and the real CREATED arrives, hasCreatedAction becomes true and shouldAddCreatedAction returns to false, so we don’t double-inject.
  1. 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 small ReportActionsSkeletonView directly under the CREATED action: Add initial-load flags (above the renderItem):
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 CREATED action),
  • 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)

marufsharifi avatar Dec 09 '25 09:12 marufsharifi

@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
Image Image
  • show loading on expense open
Before After
Image Image

I'm not sure if it's expected behaviour.

VickyStash avatar Dec 09 '25 09:12 VickyStash

Proposal demo video

https://github.com/user-attachments/assets/c45c1728-9295-4442-a86a-3428cf4718a4

marufsharifi avatar Dec 09 '25 09:12 marufsharifi

^^ 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
Image Image

@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

trjExpensify avatar Dec 09 '25 12:12 trjExpensify

@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

LorenzoBloedow avatar Dec 09 '25 12:12 LorenzoBloedow

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

suhailpthaj avatar Dec 09 '25 12:12 suhailpthaj

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

marufsharifi avatar Dec 09 '25 14:12 marufsharifi

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.

dannymcclain avatar Dec 09 '25 14:12 dannymcclain

@sobitneupane Huh... This is 4 days overdue. Who can take care of this?

melvin-bot[bot] avatar Dec 09 '25 23:12 melvin-bot[bot]

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.

sobitneupane avatar Dec 10 '25 09:12 sobitneupane

@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

marufsharifi avatar Dec 10 '25 09:12 marufsharifi

@sobitneupane I was the first one to mention skeleton loader approach.

suhailpthaj avatar Dec 10 '25 12:12 suhailpthaj

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!

trjExpensify avatar Dec 10 '25 13:12 trjExpensify

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

LorenzoBloedow avatar Dec 10 '25 14:12 LorenzoBloedow

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.

dannymcclain avatar Dec 10 '25 15:12 dannymcclain

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

mountiny avatar Dec 11 '25 03:12 mountiny

@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:

  1. 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.
  2. 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.
  3. Leave the original behavior unchanged – no header until OpenReport finishes (as in the current production behavior).

marufsharifi avatar Dec 11 '25 06:12 marufsharifi

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

dubielzyk-expensify avatar Dec 11 '25 08:12 dubielzyk-expensify

@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?

mountiny avatar Dec 11 '25 14:12 mountiny

📣 It's been a week! Do we have any satisfactory proposals yet? Do we need to adjust the bounty for this issue? 💸

melvin-bot[bot] avatar Dec 11 '25 16:12 melvin-bot[bot]