Skip to content

[Due for payment 2025-12-26] [$250] Download icon/option not show in image preview (for large file size image) #74128

Closed

Description

@m-natarajan

If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!


Version Number: 9.2.42-11
Reproducible in staging?: Y
Reproducible in production?: Y
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: @mallenexpensify
Slack conversation (hyperlinked to channel name): #Expensify Bugs

Action Performed:

  1. Send any lengthy/large attachment to User B
  2. Login in as User B
  3. Open the attachment and observe

Expected Result:

To be able to download the image

Actual Result:

No download option showing in upper right hand corner

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
  • 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
  • Windows: Chrome
  • MacOS: Chrome / Safari
  • MacOS: Desktop

Screenshots/Videos

Add any screenshot/video evidence
Image Image

View all open jobs on GitHub

Upwork Automation - Do Not Edit
  • Upwork Job URL: https://www.upwork.com/jobs/~021985737100474543832
  • Upwork Job ID: 1985737100474543832
  • Last Price Increase: 2025-11-11
  • Automatic offers:
    • huult | Contributor | 109469080
    • truph01 | Contributor | 109722211
Issue OwnerCurrent Issue Owner: @joekaufmanexpensify

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Nov 3, 2025
  2. melvin-bot commented on Nov 3, 2025

    @melvin-bot

    Triggered auto assignment to @joekaufmanexpensify (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.

  3. nyomanjyotisa commented on Nov 4, 2025

    @nyomanjyotisa
    Member

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    Download icon/option not show in image preview (for large file size image)

    What is the root cause of that problem?

    When an image is rendered, this useEffect sets the attachment loaded state to false on mount

    useEffect(() => {
    setAttachmentLoaded?.(source as AttachmentSource, false);
    // eslint-disable-next-line react-compiler/react-compiler
    // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    But the problem occurs because the Lightbox component renders two Image components simultaneously:

    1. The main image inside MultiGestureCanvas

    <Image
    source={{uri}}
    style={[contentSize ?? styles.invisibleImage]}
    isAuthTokenRequired={isAuthTokenRequired}
    onError={onError}
    onLoad={(e) => {
    updateContentSize(e);
    setLightboxImageLoaded(true);
    }}
    waitForSession={() => {
    // only active lightbox should call this function
    if (!isActive || isFallbackVisible || !isLightboxVisible) {
    return;
    }
    setContentSize(cachedImageDimensions.get(uri));
    setLightboxImageLoaded(false);
    }}
    />

    1. A fallback image

    {isFallbackVisible && (
    <View style={StyleUtils.getFullscreenCenteredContentStyles()}>
    <Image
    source={{uri}}
    resizeMode="contain"
    style={[fallbackSize ?? styles.invisibleImage]}
    isAuthTokenRequired={isAuthTokenRequired}
    onLoad={(e) => {
    updateContentSize(e);
    setFallbackImageLoaded(true);
    }}
    />
    </View>

    For large images that load instantly from cache:

    1. The first Image component mounts and sets loaded state to false
    2. The image loads immediately and sets it to true
    3. The second Image component (fallback) mounts and sets it back to false
    4. Then the download button (which depends on isAttachmentLoaded?.(source)) flickers and disappears

    const {isAttachmentLoaded} = useContext(AttachmentStateContext);
    const shouldShowDownloadButton = useMemo(() => {
    const isValidContext = !isEmptyObject(report) || type === CONST.ATTACHMENT_TYPE.SEARCH;
    if (!isValidContext || isErrorInAttachment(source)) {
    return false;
    }
    return !!onDownloadAttachment && isDownloadButtonReadyToBeShown && !shouldShowNotFoundPage && !isOffline && !isLocalSource && isAttachmentLoaded?.(source);
    }, [isAttachmentLoaded, isDownloadButtonReadyToBeShown, isErrorInAttachment, isLocalSource, isOffline, onDownloadAttachment, report, shouldShowNotFoundPage, source, type]);

    What changes do you think we should make in order to solve the problem?

    We should prevent the BaseImage component from setting the attachment loaded state to false if it's already true. So, when a large image loads quickly from the main Image component, the fallback Image component won't reset it back to false

        const {setAttachmentLoaded, isAttachmentLoaded} = useContext(AttachmentStateContext);
        useEffect(() => {
            // Only set to false if it's not already loaded
            if (!isAttachmentLoaded?.(source as AttachmentSource)) {
                setAttachmentLoaded?.(source as AttachmentSource, false);
            }
            // eslint-disable-next-line react-compiler/react-compiler
            // eslint-disable-next-line react-hooks/exhaustive-deps
        }, []);

    const {setAttachmentLoaded} = useContext(AttachmentStateContext);
    useEffect(() => {
    setAttachmentLoaded?.(source as AttachmentSource, false);
    // eslint-disable-next-line react-compiler/react-compiler
    // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    Apply the same change to BaseImage.native.tsx

    What alternative solutions did you explore? (Optional)

    None

  4. changed the title [-]Download icon/option not show in image preview (for large file size image)[/-] [+][$250] Download icon/option not show in image preview (for large file size image)[/+] on Nov 4, 2025
  5. melvin-bot commented on Nov 4, 2025

    @melvin-bot
  6. added
    Help WantedApply this label when an issue is open to proposals by contributors
    on Nov 4, 2025
  7. melvin-bot commented on Nov 4, 2025

    @melvin-bot

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

  8. huult commented on Nov 5, 2025

    @huult
    Contributor

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    Download icon/option not show in image preview (for large file size image)

    What is the root cause of that problem?

    In high-resolution cases, we use the imageSource prop here:

    imageSource = SafeString(previewSource) || imageSource;

    This imageSource is passed to the BaseImage component, where isAttachmentLoaded is set to true once the image is loaded:

    setAttachmentLoaded?.(source as AttachmentSource, true);

    For high-resolution images, the imageSource includes the 1024.jpg variant.

    Later, when isAttachmentLoaded is checked in AttachmentModalBaseContent, it uses a different source value (without 1024.jpg), so the comparison fails and isAttachmentLoaded returns false. This happens because we use the source, not the preview source.

    As a result, the download button does not appear even though the image is loaded.

    return !!onDownloadAttachment && isDownloadButtonReadyToBeShown && !shouldShowNotFoundPage && !isOffline && !isLocalSource && isAttachmentLoaded?.(source);

    What changes do you think we should make in order to solve the problem?

    To resolve this issue, we should update the source to check isAttachmentLoaded in the case of high-resolution images as well. Update the logic to ensure that for high-resolution images, we check the correct image source (previewSource) when determining whether the attachment has loaded

    const shouldShowDownloadButton = useMemo(() => {
    const isValidContext = !isEmptyObject(report) || type === CONST.ATTACHMENT_TYPE.SEARCH;
    if (!isValidContext || isErrorInAttachment(source)) {
    return false;
    }
    return !!onDownloadAttachment && isDownloadButtonReadyToBeShown && !shouldShowNotFoundPage && !isOffline && !isLocalSource && isAttachmentLoaded?.(source);
    }, [isAttachmentLoaded, isDownloadButtonReadyToBeShown, isErrorInAttachment, isLocalSource, isOffline, onDownloadAttachment, report, shouldShowNotFoundPage, source, type]);

    update to

     const shouldShowDownloadButton = useMemo(() => {
            const isValidContext = !isEmptyObject(report) || type === CONST.ATTACHMENT_TYPE.SEARCH;
            if (!isValidContext || isErrorInAttachment(source)) {
                return false;
            }
    
            // When isHighResolution is true, check if previewSource is loaded instead of the main source
            let sourceToCheck: AvatarSource = source;
            if (isHighResolution && previewSource) { 
                const previewSourceString = SafeString(previewSource);
                sourceToCheck = previewSourceString || source;
            }
            return !!onDownloadAttachment && isDownloadButtonReadyToBeShown && !shouldShowNotFoundPage && !isOffline && !isLocalSource && isAttachmentLoaded?.(sourceToCheck);
        }, [
            isAttachmentLoaded,
            isDownloadButtonReadyToBeShown,
            isErrorInAttachment,
            isLocalSource,
            isOffline,
            onDownloadAttachment,
            report,
            shouldShowNotFoundPage,
            source,
            type,
            isHighResolution,
            previewSource,
        ]);

    Test branch

    Screen.Recording.2025-11-05.at.15.58.23.mov

    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.

  9. IjaazA commented on Nov 5, 2025

    @IjaazA
    Contributor

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    The download icon/option is not showing in the image preview for large file size images. When a user opens a large attachment, the download button in the upper right-hand corner of the modal does not appear, preventing users from downloading the image.

    What is the root cause of that problem?

    The AttachmentModal component does not verify if the attachment has finished loading before rendering the download button.

    The current logic only checks for:

    allowDownload flag
    isDownloadButtonReadyToBeShown (carousel state)
    Other UI conditions like offline mode, local source, etc.
    For large images, the button visibility logic runs before the image finishes loading, and since there’s no check for the attachment’s load state, the button never appears.
    The newer AttachmentModalBaseContent component already has load-state tracking implemented via AttachmentStateContext, but AttachmentModal (standalone modal) does not.

    What changes do you think we should make in order to solve the problem?

    Add attachment load state tracking to AttachmentModal by:
    Wrapping modal content with AttachmentStateContextProvider.
    Adding a check for isAttachmentLoaded before showing the download button.
    Moving the button visibility logic into an inner component that can access the context.
    Implementation Strategy

    1. Add Attachment Load State Tracking
      Import and use AttachmentStateContextProvider and AttachmentStateContext from the attachment modal screen components.
      Wrap the modal content with the provider to track when attachments finish loading.

    2. Add Load State Check
      Update shouldShowDownloadButton logic to include a check for whether the image has fully loaded using isAttachmentLoaded?.(sourceState).

    3. Create Inner Component
      Create a new inner component AttachmentModalContent inside AttachmentModal.tsx.
      Use useContext(AttachmentStateContext) inside it to access isAttachmentLoaded.
      Move the button rendering logic into this component so it re-renders when load state updates.

    4. Update Button Visibility Logic
      Before:

    shouldShowDownloadButton = allowDownload &&
        isDownloadButtonReadyToBeShown &&
        !shouldShowNotFoundPage &&
        !isReceiptAttachment &&
        !isOffline &&
        !isLocalSource;
    

    After:

    const {isAttachmentLoaded} = useContext(AttachmentStateContext);
    shouldShowDownloadButton = allowDownload &&
        isDownloadButtonReadyToBeShown &&
        !shouldShowNotFoundPage &&
        !isReceiptAttachment &&
        !isOffline &&
        !isLocalSource &&
        isAttachmentLoaded?.(sourceState as AttachmentSource);
    

    What alternative solutions did you explore? (Optional)
    Timeout-based approach
    Wait for a fixed timeout before showing the download button.

  10. joekaufmanexpensify commented on Nov 6, 2025

    @joekaufmanexpensify
    Contributor

    Pending @jayeshmangwani review

  11. 43 remaining items

  12. melvin-bot commented on Dec 19, 2025

    @melvin-bot

    The solution for this issue has been 🚀 deployed to production 🚀 in version 9.2.84-8 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:

    If no regressions arise, payment will be issued on 2025-12-26. 🎊

    For reference, here are some details about the assignees on this issue:

  13. melvin-bot commented on Dec 19, 2025

    @melvin-bot

    @huult / @truph01 @joekaufmanexpensify The PR fixing this issue has been merged! The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the BugZero Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]

  14. joekaufmanexpensify commented on Dec 22, 2025

    @joekaufmanexpensify
    Contributor

    @truph01 could you please handle the checklist so we can prep for payment?

  15. truph01 commented on Dec 22, 2025

    @truph01
    Contributor

    BugZero Checklist:

    • [Contributor] The offending PR has been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.

      Link to comment:

    • [Contributor] If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in #expensify-open-source has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner.

      Link to discussion:

    • [Contributor] If it was decided to create a regression test for the bug, please propose the regression test steps using the template below to ensure the same bug will not reach production again.

    • [BugZero Assignee] Create a GH issue for creating/updating the regression test once above steps have been agreed upon.

      Link to issue:

    Regression Test Proposal

    Precondition:

    Test:

    1. Open the Expensify app.

    2. Open any chat and send a large or lengthy attachment. Example:
      Attachment

    3. Open the attachment.

    4. Verify that the download icon is visible and that you are able to download the image.

    Do we agree 👍 or 👎

  16. nyomanjyotisa commented on Dec 22, 2025

    @nyomanjyotisa
    Member

    It seems like the latest PR uses the same logic as I originally proposed (which wasn't selected at the time). Even if it's not identical, both check if the attachment is loaded before setting it to false, just written with different syntax (negative check vs early return)

    Since the final solution implements this logic, am I eligible for partial payment/compensation?

    Thanks!

    cc @joekaufmanexpensify @truph01

  17. joekaufmanexpensify commented on Dec 23, 2025

    @joekaufmanexpensify
    Contributor

    My initial thought is no. It's not unusual for there to be some overlap between solutions for a bug, and I am not aware of such overlap in and of itself being grounds for partial compensation. cc @pecanoro in case you feel differently.

  18. joekaufmanexpensify commented on Dec 23, 2025

    @joekaufmanexpensify
    Contributor

    @truph01 thanks for doing checklist.

  19. joekaufmanexpensify commented on Dec 26, 2025

    @joekaufmanexpensify
    Contributor
  20. joekaufmanexpensify commented on Dec 26, 2025

    @joekaufmanexpensify
    Contributor

    All set to pay 👍

  21. joekaufmanexpensify commented on Dec 26, 2025

    @joekaufmanexpensify
    Contributor

    Payment Summary

    • Contributor: @huult paid $250 via Upwork
    • Contributor+: @truph01 due $250 via Upwork
  22. joekaufmanexpensify commented on Dec 26, 2025

    @joekaufmanexpensify
    Contributor

    @huult @truph01 I sent $250 to both of you via Upwork and ended your contracts

  23. joekaufmanexpensify commented on Dec 26, 2025

    @joekaufmanexpensify
    Contributor

    Upwork job closed

  24. joekaufmanexpensify commented on Dec 26, 2025

    @joekaufmanexpensify
    Contributor

    All set, thanks everyone!

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

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionBugSomething is broken. Auto assigns a BugZero manager.ExternalAdded to denote the issue can be worked on by a contributorWeeklyKSv2

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions