Skip to content

[Due for payment 2026-06-25] [$250] Chat - Offline message not displayed when opening video offline #88216

Closed

Description

@jponikarchuk

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.3.60-15
Reproducible in staging?: Yes
Reproducible in production?: No
If this was caught during regression testing, add the test name, ID and link from BrowserStack: https://test-management.browserstack.com/projects/2219752/test-runs/TR-20165/folder/13176710/41236940/1343291839?sort=%2Bidentifier
Email or phone of affected tester (no customers): jsat+6abr@applausemail.com
Issue reported by: Applause Internal Team
Bug source: Regression TC Execution
Device used: iPhone 13 mini/18.6.2
App Component: Other

Action Performed:

  1. Launch Expensify App
  2. Log in with valid credentials
  3. Navigate to any chat
  4. Upload a video
  5. Disable the internet connection via troubleshoot
  6. Click on the video

Expected Result:

An offline message is displayed and download option is not displayed

Actual Result:

The video can be reproduced, the offline message and download option are not displayed

Workaround:

Unknown

Platforms:

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome / Safari

Screenshots/Videos

Video.mp4

View all open jobs on GitHub

Issue OwnerCurrent Issue Owner: @mallenexpensify
Upwork Automation - Do Not Edit
  • Upwork Job URL: https://www.upwork.com/jobs/~022045202730445059794
  • Upwork Job ID: 2045202730445059794
  • Last Price Increase: 2026-04-24

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Apr 17, 2026
  2. MelvinBot commented on Apr 17, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    The offline indicator for videos is gated on having zero buffered content. In BaseVideoPlayer.tsx:158-160:

    const shouldShowOfflineIndicator = useMemo(() => {
        return isVideoOffline && currentTime + bufferedPosition <= 0;
    }, [bufferedPosition, currentTime, isVideoOffline]);

    On web browsers, the HTML5 video element retains its buffered data even after the app goes offline (via Troubleshoot "Force offline" or real network loss). Since bufferedPosition > 0, the condition is never satisfied and the offline indicator never appears.

    Similarly, the video source replacement at line 127-132 only fires when isLoading is also true — but a fully-buffered video is in readyToPlay state, not loading, so the source is never cleared either.

    This doesn't reproduce on native iOS/Android because expo-video's native player doesn't retain buffer state the same way the browser's <video> element does.

    Note: The download button is already correctly hidden when offline — both in the attachment modal header and the video popover menu. The bug is isolated to video playback continuing and the offline overlay not appearing.

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

    The shouldShowOfflineIndicator condition in BaseVideoPlayer.tsx should not require currentTime + bufferedPosition <= 0 when the device is offline. When isVideoOffline is true (after the 7-second OFFLINE_THRESHOLD), the offline indicator should show regardless of the browser's buffer state.

    Change the condition at BaseVideoPlayer.tsx:158-160 to:

    shouldShowOfflineIndicator = isVideoOffline
    

    And update the replaceAsync('') effect at lines 127-132 to also fire when the video is not loading but is offline — i.e., remove the isLoading guard so that when isVideoOffline && isOffline, the video source is always cleared, stopping playback and preventing the user from interacting with the buffered video.

    What alternative solutions did you explore? (Optional)

    An alternative would be to gate video rendering at the AttachmentView level (in AttachmentView/index.tsx:370-381) by checking isOffline before rendering AttachmentViewVideo at all — showing AttachmentOfflineIndicator directly instead. However, this would be a larger change and would bypass the existing offline detection mechanism in BaseVideoPlayer that handles the 7-second delay and works correctly on native platforms. Fixing the condition inside BaseVideoPlayer is more surgical and keeps the existing architecture intact.


    Next Steps for Contributor+ team: Reply with @MelvinBot implement this to create a draft PR, @MelvinBot <your feedback> to refine this analysis, or explain why you are rejecting Melvin's proposal.

  3. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Apr 17, 2026
  4. added
    Help WantedApply this label when an issue is open to proposals by contributors
    on Apr 17, 2026
  5. melvin-bot commented on Apr 17, 2026

    @melvin-bot

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

  6. changed the title [-]Chat - Offline message not displayed when opening video offline[/-] [+][$250] Chat - Offline message not displayed when opening video offline[/+] on Apr 17, 2026
  7. melvin-bot commented on Apr 17, 2026

    @melvin-bot
  8. JustDreameritis commented on Apr 17, 2026

    @JustDreameritis

    Proposal

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

    When a user goes offline and taps on a video in chat, the video continues playing from its buffer instead of showing the offline indicator message. The download option hiding works correctly, but the offline overlay never appears on web platforms.

    What is the root cause of that problem?

    The offline indicator condition in BaseVideoPlayer.tsx:158-160 requires currentTime + bufferedPosition <= 0, but on web browsers the HTML5 <video> element retains its buffered data after going offline, so bufferedPosition stays positive and the indicator never shows. The replaceAsync('') call at line 127-132 that would clear the source is also gated on isLoading, which is false for an already-buffered video in readyToPlay state.

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

    Two changes in BaseVideoPlayer.tsx, single-file fix:

    1. Simplify the offline indicator condition (line 158-160) to show whenever the video is confirmed offline, regardless of buffer state:
    const shouldShowOfflineIndicator = useMemo(() => {
        return isVideoOffline;
    }, [isVideoOffline]);
    1. Remove the isLoading guard from the replaceAsync('') effect (line 127-132) so the video source is cleared whenever isVideoOffline && isOffline is true, even if the video is already in readyToPlay state. This stops playback of buffered content:
    useEffect(() => {
        if (!(isVideoOffline && isOffline)) {
            return;
        }
        videoPlayerRef.current.replaceAsync('');
    }, [isVideoOffline, isOffline]);

    This ensures that on web, once the 7-second OFFLINE_THRESHOLD elapses, the video is stopped and the offline overlay appears — matching native behavior where the buffer is not retained.

    What alternative solutions did you explore? (Optional)

    Gating at the AttachmentView level by checking isOffline before rendering AttachmentViewVideo would also work but bypasses the existing 7-second threshold delay logic in BaseVideoPlayer and creates a larger change surface across components.

  9. skylarkerx commented on Apr 17, 2026

    @skylarkerx

    1. Problem description

    After uploading a video in a chat, forcing the app offline (e.g. via troubleshoot) and opening the video shows playback from cache/buffer, but the offline attachment messaging does not appear and download-related UI does not match the expected offline state (modal / custom controls vs. native controls differ by platform).

    2. What is the root cause of that problem?

    A. Offline overlay is suppressed whenever the player already has time or buffer

    After any decode/buffer, shouldShowOfflineIndicator stays false while offline, so AttachmentOfflineIndicator never shows even though playback can continue (BaseVideoPlayer.tsx L158–L160):

        const shouldShowOfflineIndicator = useMemo(() => {
            return isVideoOffline && currentTime + bufferedPosition <= 0;
        }, [bufferedPosition, currentTime, isVideoOffline]);

    B. “Offline” for the player UI is delayed by several seconds

    BaseVideoPlayer.tsx (L114–L125) sets isVideoOffline only after CONST.VIDEO_PLAYER.OFFLINE_THRESHOLD (7000 ms), so the offline UI lags behind useNetwork().isOffline.

    C. Native mobile: no nodownload on native fullscreen controls

    useHandleNativeVideoControls/index.native.ts is a no-op, while the web implementation sets controlsList="nodownload" when offline. BaseVideoPlayer enables nativeControls in fullscreen, so browsers/OS may still expose download where web custom logic would hide it.

    Note: The video popover already omits download when offline (VideoPopoverMenuContext.tsx L70–78), and the attachment modal hides the download button when offline (AttachmentModalBaseContent/index.tsx L190–197)—so gaps are mainly the overlay gating/delay and native fullscreen.

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

    Patch 1 — Show offline overlay for remote video whenever we treat the session as offline for this player

    Decouple “show offline UX” from “no bytes buffered yet.” For example, drive the overlay from isVideoOffline (or isOffline) and !isUploading (local blob uploads already use ATTACHMENT_LOCAL_URL_PREFIX), and remove the currentTime + bufferedPosition <= 0 requirement, or replace it with a narrower check (e.g. only skip overlay for purely local preview URLs).

    Brief sketch:

     const shouldShowOfflineIndicator = useMemo(() => {
    -    return isVideoOffline && currentTime + bufferedPosition <= 0;
    -}, [bufferedPosition, currentTime, isVideoOffline]);
    +    return isVideoOffline && !isUploading;
    +}, [isVideoOffline, isUploading]);

    Re-test: controls block that checks !shouldShowOfflineIndicator (L598–600) should still hide custom controls when the overlay shows.

    Patch 2 — Align offline UI timing with the rest of the app

    Either reduce OFFLINE_THRESHOLD for video-only UX, or use isOffline for AttachmentOfflineIndicator while keeping delayed isVideoOffline only for replaceAsync('') (L127–132) if that delay is still required for cache detection.

    Patch 3 — Native fullscreen

    Implement native equivalent of hiding download (platform-specific fullscreen / VideoView options), or force custom controls when offline so behavior matches web.

    4. An alternative considered

    Aggressive: On isOffline for non-local URLs, call pause(), clear/replace the source (similar to L127–132), and rely on AttachmentOfflineIndicator + no playback until back online. This matches “no playback while offline” but is harsher if product wants to allow buffered-only viewing with a clear offline banner—Patch 1 matches the stated expected result without blocking cache playback entirely.

  10. KJ21-ENG commented on Apr 17, 2026

    @KJ21-ENG
    Contributor

    🚫 Duplicated proposal withdrawn by 🤖 ProposalPolice.

  11. github-actions commented on Apr 17, 2026

    @github-actions
    Contributor

    ⚠️ @KJ21-ENG Your proposal is a duplicate of an already existing proposal and has been automatically withdrawn to prevent spam. Please review the existing proposals before submitting a new one.

  12. wildan-m commented on Apr 17, 2026

    @wildan-m
    Contributor

    Proposal

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

    When a chat video attachment is opened on web after the device goes offline, the fullscreen attachment viewer silently plays the video from the browser HTTP cache instead of showing the offline indicator. The user never sees the "You appear to be offline" message that is expected for other offline attachment interactions.

    Affected platforms: Android mWeb Chrome, iOS mWeb Safari / Chrome, Windows Chrome, macOS Chrome/Safari. Native iOS App and native Android App are not checked in the report, which is consistent with the root cause being specific to the browser's HTTP media cache.

    What is the root cause of that problem?

    There are two cooperating conditions in the video player that together suppress the offline indicator after a fresh upload.

    Condition 1 — the preview on web silently warms the browser HTTP cache with the freshly uploaded video. On web, after upload, VideoPlayerPreview creates a hidden <video> element and calls .load() on it just to measure the real video dimensions:

    useEffect(() => {
    if (!videoUrl || getPlatform() !== CONST.PLATFORM.WEB) {
    return;
    }
    const video = document.createElement('video');
    video.onloadedmetadata = () => {
    if (video.videoWidth === videoDimensions.width && video.videoHeight === videoDimensions.height) {
    return;
    }
    setWebMeasuredDimensions({
    width: video.videoWidth,
    height: video.videoHeight,
    });
    };
    video.src = videoUrl;
    video.load();
    return () => {
    video.src = '';
    };
    }, [videoUrl, videoDimensions.width, videoDimensions.height]);

    That measurement request is enough for the browser to cache the video bytes (and the HTTP response) against the encrypted URL even though the chat surface only ever displays a static thumbnail in narrow layout. Once the user goes offline and taps the thumbnail, the fresh BaseVideoPlayer instance in the attachment modal re-requests the same URL and the browser serves it from its HTTP media cache. This does not happen on native App (no hidden <video> element, no web HTTP cache) — matching the "Platforms" checklist in the report.

    Condition 2 — BaseVideoPlayer's shouldShowOfflineIndicator is only true while nothing has buffered, and isVideoOffline only becomes true 7 seconds after isOffline flips. On mount in the attachment modal:

    const shouldShowOfflineIndicator = useMemo(() => {
    return isVideoOffline && currentTime + bufferedPosition <= 0;
    }, [bufferedPosition, currentTime, isVideoOffline]);

    shouldShowOfflineIndicator requires currentTime + bufferedPosition <= 0. Because the bytes are served from the browser cache, the player immediately reports a non-zero buffered position and, via the readyToPlay auto-play branch, starts incrementing currentTime — so the sum is never <= 0.

    useEventListener(videoPlayerRef.current, 'statusChange', (payload: StatusChangeEventPayload) => {
    if (payload.status === 'error') {
    setHasErrorIconVisible(true);
    } else {
    updatePlayerStatus(payload.status);
    }
    if (payload.status !== 'readyToPlay') {
    return;
    }
    isReadyForDisplayRef.current = true;
    setHasErrorIconVisible(false);
    if (isFirstLoad) {
    setIsFirstLoad(false);
    if (videoPlayerRef.current === currentVideoPlayerRef.current && !isUploading) {
    playVideo();
    }
    }
    });

    That auto-play branch plays the video whenever the status reaches readyToPlay, with no check against the network state, so a cached video starts playing the moment the modal opens.

    Compounding both conditions, isVideoOffline is initialized to false and only flips to true 7 seconds after isOffline becomes true:

    useEffect(() => {
    if (!isOffline) {
    setIsVideoOffline(false);
    return;
    }
    const timer = setTimeout(() => {
    setIsVideoOffline(true);
    }, CONST.VIDEO_PLAYER.OFFLINE_THRESHOLD);
    return () => clearTimeout(timer);
    }, [isOffline]);

    So even if the buffer check were relaxed, the indicator could not appear for 7 seconds on a newly mounted player. By the time isVideoOffline is finally true, cached playback has already produced currentTime > 0 and bufferedPosition > 0, and the indicator stays suppressed forever.

    The same AttachmentOfflineIndicator component is already used correctly for non-cached flows, so the regression is entirely in how BaseVideoPlayer derives shouldShowOfflineIndicator and gates the auto-play when the player is mounted while the device is already confirmed offline. The 7-second OFFLINE_THRESHOLD was introduced to absorb intermittent network blips during ongoing playback — it was not meant to apply to the "mount in an already-offline state" case.

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

    Treat "mounted while already offline" as a distinct, confirmed-offline case in BaseVideoPlayer, rather than waiting 7 seconds and relying on buffered position.

    Concretely:

    1. In src/components/VideoPlayer/BaseVideoPlayer.tsx, make the "is confirmed offline" state reflect the initial network state instead of always starting at false. If the player is constructed while isOffline is already true, treat the offline state as confirmed immediately (the 7-second threshold is only needed for in-flight flips during playback, not for a fresh mount). The existing effect that watches isOffline and sets the timer remains in charge of the flip case.

    2. Stop auto-playing when the player reaches readyToPlay while isOffline is true. Today the statusChange listener calls playVideo() whenever isFirstLoad is true and the player is the current one; it should additionally require that the network is online. This ensures currentTime stays at 0 for a cached-but-offline video and prevents the user from silently consuming a cached copy without being told they are offline.

    3. Simplify shouldShowOfflineIndicator so it no longer depends on currentTime + bufferedPosition <= 0. Show the indicator whenever the player is confirmed offline and the user is not actively watching (i.e., not currently playing) and we are not uploading a new video. Combined with change 2, this means: the indicator appears as soon as a cached video is opened while offline, stays hidden while a video continues to play through an intermittent network blip, and reappears once playback stalls because the buffer has exhausted.

    Together, these three adjustments give the modal the same observable behaviour a user would see for a non-cached video: the offline message appears, the built-in download affordance stays hidden (the existing shouldShowDownloadButton check in AttachmentModalBaseContent already gates on !isOffline), and the player refuses to silently play cached bytes that bypass the online/offline distinction.

    The fix only touches src/components/VideoPlayer/BaseVideoPlayer.tsx. No changes are required in AttachmentOfflineIndicator.tsx, AttachmentView, AttachmentModalBaseContent, or the preview — those surfaces all respond correctly once BaseVideoPlayer reports the right shouldShowOfflineIndicator value and stops auto-playing.

    Compare branch: https://github.com/Expensify/App/compare/main...wildan-m:App:wildan/88216-video-offline-indicator-missing?expand=1

    What alternative solutions did you explore? (Optional)

    Forcing VideoPlayerPreview to skip the hidden <video> dimension probe when offline was considered, because it is that probe which seeds the browser HTTP cache. This was rejected because (a) the cache is also seeded the first time the user views the thumbnail on a wide layout and (b) any future path that ever fetches the URL while online would still poison the cache. The proper place to authoritatively decide "we are offline, do not play" is the player itself, not every upstream consumer.

    Clearing the video source via replaceAsync('') on mount when offline was also considered. The existing replaceAsync('') effect already runs when the player is loading while offline, but it is conditioned on isVideoOffline && isLoading && isOffline, so it only fires after the 7-second threshold and only while status is still loading. By the time a cached source reaches readyToPlay, isLoading is already false and that effect never runs. Forcing the source to be cleared would also interact badly with the reconnect flow, which expects to restore the original sourceURL only when currentTime <= 0 && hasError. Fixing shouldShowOfflineIndicator and the auto-play gate keeps the reconnect path intact and avoids the need to reason about an empty source.

  13. 51 remaining items

  14. Ollyws commented on Jun 30, 2026

    @Ollyws
    Contributor

    Hmm Melvin isn't listening.

  15. Ollyws commented on Jun 30, 2026

    @Ollyws
    Contributor

    @MelvinBot Tell me which PR caused this bug.

  16. Ollyws commented on Jun 30, 2026

    @Ollyws
    Contributor

    BugZero Checklist:

    • [Contributor] Classify the bug:
    Bug classification

    Source of bug:

    • 1a. Result of the original design (eg. a case wasn't considered)
    • 1b. Mistake during implementation
    • 1c. Backend bug
    • 1z. Other:

    Where bug was reported:

    • 2a. Reported on production
    • 2b. Reported on staging (deploy blocker)
    • 2c. Reported on both staging and production
    • 2d. Reported on a PR
    • 2z. Other:

    Who reported the bug:

    • 3a. Expensify user
    • 3b. Expensify employee
    • 3c. Contributor
    • 3d. QA
    • 3z. Other:
    • [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: https://github.com/Expensify/App/pull/75270/changes#r3500156403

    • [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.

    Regression Test Proposal Template

    Regression Test Proposal

    Precondition:

    Test:

    1. Navigate to any chat
    2. Upload a video
    3. Disable the internet connection via troubleshoot
    4. Click on the video
    5. Verify that an offline message is displayed and download option is not displayed
    

    Do we agree 👍 or 👎

    Zapier Logs Run ID: 00040eee-0ac3-a238-6e8a-ac96565599b4
    Zapier Logs Run ID: 00040eee-9e2d-a585-e161-2f6abf9201d4
  17. Ollyws commented on Jun 30, 2026

    @Ollyws
    Contributor

    @mallenexpensify checklist done and payment requested.

  18. nabi-ebrahimi commented on Jul 2, 2026

    @nabi-ebrahimi

    @mallenexpensify, could you please reopen this issue? I haven’t been paid yet.

    You already hired me here: #88216 (comment), but I still haven’t received the payment.

    Thank you!

  19. mallenexpensify commented on Jul 2, 2026

    @mallenexpensify
    Contributor

    @nabi-ebrahimi paid, summary updated above. Thx.

    ^ Test case created. Thx

  20. flaviadefaria commented on Jul 7, 2026

    @flaviadefaria
    Contributor

    Approved: @Ollyws due $250 via NewDot

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.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributor

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions