Skip to content

fix: video attachments not reproducing on iOS - #7530

Open
OtavioStasiak wants to merge 11 commits into
developfrom
fix.video-not-playing
Open

fix: video attachments not reproducing on iOS#7530
OtavioStasiak wants to merge 11 commits into
developfrom
fix.video-not-playing

Conversation

@OtavioStasiak

@OtavioStasiak OtavioStasiak commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Proposed changes

Media attachments could keep pointing at the remote URL even after the file was already cached on disk, so opening them streamed from the server instead of playing the local file. On iOS that surfaced as a video thumbnail that spins
forever with no error.

Root cause. useFile only merged local overrides for non-persisted messages, relying on persistMessage to write the downloaded uri back to the database for everything else. That write silently no-ops whenever no row matches the message id, which is the case for forwarded messages and for the Files/Mentions/Starred/Pinned lists —MessagesView builds attachments from the uploads REST payload, which has _id but no id and no message id at all, so useMessageId() is undefined. Downloading from the Files tab therefore left the room's message row holding the remote URL while the file sat fully downloaded on disk.

Why only iOS, and only some videos. The broken state is identical on both platforms; the difference is what each player does when asked to stream the file. An MP4/MOV can't start until the player has the moov atom (sample
tables, byte offsets, durations). macOS screen recordings and az_recorder write moov at the end of the file — they can't know the sample table until recording stops — whereas the iPhone camera writes faststart (moov first).
Reaching a trailing moov over HTTP requires working Range requests. AVFoundation assumes Range works and simply waits: the item stays in AVPlayerItemStatusUnknown, so expo-av never fires onLoad (needs ReadyToPlay) or onError (needs AVPlayerItemStatusFailed), and loading stays true. ExoPlayer falls back to reading forward from byte 0 until it finds moov — wasteful, but it terminates, so Android just loaded slowly. The absence of an error alert was the tell: a 404/403 would have popped the view with Error_play_video.

Issue(s)

https://rocketchat.atlassian.net/browse/NATIVE-1307

How to test or reproduce

  • Open the app;
  • Send a screen Record from macOS;
  • Try to reproduce it on RoomView;

Screenshots

Before After
before_video_ios after_video_ios

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • Improvement (non-breaking change which improves a current function)
  • New feature (non-breaking change which adds functionality)
  • Documentation update (if none of the other choices apply)

Checklist

  • I have read the CONTRIBUTING doc
  • I have signed the CLA
  • Lint and unit tests pass locally with my changes
  • I have added tests that prove my fix is effective or that my feature works (if applicable)
  • I have added necessary documentation (if applicable)
  • Any dependent changes have been merged and published in downstream modules

Further comments

Summary by CodeRabbit

Bug Fixes

  • Fixed attachment URLs with spaces or special characters so images and videos load correctly.
  • Prevented double-encoding of attachment links.
  • Improved handling of protected and remote attachment URLs.
  • Added safer behavior when media updates have nothing to save.

Improvements

  • Attachment overrides now merge consistently, even when source details change.
  • Simplified media auto-download initialization.

Tests

  • Added coverage for URL encoding and attachment override behavior.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Attachment overrides now use local state. Attachment URL encoding is centralized to prevent double encoding. Media rendering uses formatted URLs directly. Empty message persistence batches are logged and skipped.

Changes

Attachment handling

Layer / File(s) Summary
Local attachment override flow
app/containers/message/hooks/useFile.tsx, app/containers/message/hooks/useMediaAutoDownload.tsx, app/containers/message/hooks/__tests__/useFile.test.ts
useFile now merges local attachment overrides without message lookups. Tests cover accumulated overrides and prop changes. The auto-download consumer uses the simplified signature.
Attachment URL encoding and rendering
app/lib/methods/helpers/formatAttachmentUrl.ts, app/lib/methods/helpers/__tests__/formatAttachmentUrl.test.ts, app/views/AttachmentView.tsx
encodeAttachmentUrl safely encodes attachment URLs. Tests cover spaces, existing encoding, and malformed escapes. Image and video rendering pass formatted URLs directly.
Missing persistence handling
app/lib/methods/handleMediaDownload.ts
persistMessage logs and returns when no database rows produce a write batch.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested labels: type: bug

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary bug fix for video attachments on iOS, matching the pull request objectives and changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • NATIVE-1307: Request failed with status code 401

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
putComment timed out

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
app/lib/methods/handleMediaDownload.ts (1)

252-262: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add regression coverage for the empty-batch path.

Mock the three lookups to return no records and assert that no database write occurs; also cover the normal update path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/methods/handleMediaDownload.ts` around lines 252 - 262, Add
regression coverage around the empty-batch guard in handleMediaDownload: mock
all three lookup operations to return no records, assert db.write and db.batch
are not called, and retain a separate test verifying the normal path performs
the expected database update.
app/lib/methods/helpers/formatAttachmentUrl.ts (2)

13-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add unit tests for encodeAttachmentUrl.

This is new, exported, and central to fixing the double-encoding bug (spaces, already-encoded paths, malformed escapes falling back to the raw url). No test file was included for it in this diff — worth covering the round-trip and fallback cases directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/methods/helpers/formatAttachmentUrl.ts` around lines 13 - 27, Add
unit tests for the exported encodeAttachmentUrl function covering plain URLs
with spaces, already percent-encoded paths without double-encoding, and
malformed escape sequences returning the original URL unchanged. Use the
project’s existing test conventions and assert each round-trip and fallback
behavior directly.

45-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate protectFiles branching logic.

The if (protectFiles) return encodeAttachmentUrl(setParamInUrl(...)); return encodeAttachmentUrl(...) pattern is repeated identically for the http and CDN/relative branches, differing only in the input url. Worth extracting to avoid the two copies drifting apart later.

♻️ Proposed extraction
+	const finalizeUrl = (url: string): string =>
+		protectFiles ? encodeAttachmentUrl(setParamInUrl({ url, token, userId })) : encodeAttachmentUrl(url);
+
 	if (attachmentUrl && attachmentUrl.startsWith('http')) {
 		if (_originalUrl && !_originalUrl.startsWith(server)) {
 			return _originalUrl;
 		}

 		if (attachmentUrl.includes('rc_token')) {
 			return encodeAttachmentUrl(attachmentUrl);
 		}

-		if (protectFiles) return encodeAttachmentUrl(setParamInUrl({ url: attachmentUrl, token, userId }));
-		return encodeAttachmentUrl(attachmentUrl);
+		return finalizeUrl(attachmentUrl);
 	}
 	let cdnPrefix = store?.getState().settings.CDN_PREFIX as string;
 	cdnPrefix = cdnPrefix?.trim();
 	if (cdnPrefix && cdnPrefix.startsWith('http')) {
 		server = cdnPrefix.replace(/\/+$/, '');
 	}
-	if (protectFiles) return encodeAttachmentUrl(setParamInUrl({ url: `${server}${attachmentUrl}`, token, userId }));
-	return encodeAttachmentUrl(`${server}${attachmentUrl}`);
+	return finalizeUrl(`${server}${attachmentUrl}`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/methods/helpers/formatAttachmentUrl.ts` around lines 45 - 58,
Refactor the attachment URL handling in formatAttachmentUrl so the duplicated
protectFiles branching is consolidated into one shared flow. First determine the
input URL for the HTTP and CDN/relative cases, then apply the existing
protected-URL transformation and encodeAttachmentUrl call once, preserving the
current behavior for both branches.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/lib/methods/handleMediaDownload.ts`:
- Around line 252-258: Update the no-message branch in handleMediaDownload to
stop logging the raw messageId. Use the project’s redacted or appropriately
leveled logger, or remove the identifier from the message while preserving the
existing return behavior.

---

Nitpick comments:
In `@app/lib/methods/handleMediaDownload.ts`:
- Around line 252-262: Add regression coverage around the empty-batch guard in
handleMediaDownload: mock all three lookup operations to return no records,
assert db.write and db.batch are not called, and retain a separate test
verifying the normal path performs the expected database update.

In `@app/lib/methods/helpers/formatAttachmentUrl.ts`:
- Around line 13-27: Add unit tests for the exported encodeAttachmentUrl
function covering plain URLs with spaces, already percent-encoded paths without
double-encoding, and malformed escape sequences returning the original URL
unchanged. Use the project’s existing test conventions and assert each
round-trip and fallback behavior directly.
- Around line 45-58: Refactor the attachment URL handling in formatAttachmentUrl
so the duplicated protectFiles branching is consolidated into one shared flow.
First determine the input URL for the HTTP and CDN/relative cases, then apply
the existing protected-URL transformation and encodeAttachmentUrl call once,
preserving the current behavior for both branches.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 01c038b0-fec7-406f-a8e2-23bd86ebec52

📥 Commits

Reviewing files that changed from the base of the PR and between 7ae3df2 and 653bbd5.

📒 Files selected for processing (6)
  • app/containers/message/hooks/__tests__/useFile.test.ts
  • app/containers/message/hooks/useFile.tsx
  • app/containers/message/hooks/useMediaAutoDownload.tsx
  • app/lib/methods/handleMediaDownload.ts
  • app/lib/methods/helpers/formatAttachmentUrl.ts
  • app/views/AttachmentView.tsx
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: ESLint and Test / run-eslint-and-test
  • GitHub Check: format
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: Use descriptive names for functions, variables, and classes that clearly convey their purpose
Write comments that explain the 'why' behind code decisions, not the 'what'
Keep functions small and focused on a single responsibility
Use const by default, let when reassignment is needed, and avoid var
Prefer async/await over .then() chains for handling asynchronous operations
Use explicit error handling with try/catch blocks for async operations
Avoid deeply nested code; refactor complex logic into helper functions

Files:

  • app/containers/message/hooks/useMediaAutoDownload.tsx
  • app/views/AttachmentView.tsx
  • app/lib/methods/handleMediaDownload.ts
  • app/containers/message/hooks/useFile.tsx
  • app/lib/methods/helpers/formatAttachmentUrl.ts
  • app/containers/message/hooks/__tests__/useFile.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript for type safety; add explicit type annotations to function parameters and return types
Prefer interfaces over type aliases for defining object shapes in TypeScript
Use enums for sets of related constants rather than magic strings or numbers

Files:

  • app/containers/message/hooks/useMediaAutoDownload.tsx
  • app/views/AttachmentView.tsx
  • app/lib/methods/handleMediaDownload.ts
  • app/containers/message/hooks/useFile.tsx
  • app/lib/methods/helpers/formatAttachmentUrl.ts
  • app/containers/message/hooks/__tests__/useFile.test.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{js,jsx,ts,tsx}: Before committing changes to JavaScript or TypeScript files, run pnpm prettier-lint and TZ=UTC pnpm test for the modified files.
Use the local-first data flow: the UI reads from WatermelonDB, while sagas synchronize data with the server.
Use Redux and Redux-Saga for global or server state, and use Zustand for feature-local stores; do not assume all state is in Redux.

Files:

  • app/containers/message/hooks/useMediaAutoDownload.tsx
  • app/views/AttachmentView.tsx
  • app/lib/methods/handleMediaDownload.ts
  • app/containers/message/hooks/useFile.tsx
  • app/lib/methods/helpers/formatAttachmentUrl.ts
  • app/containers/message/hooks/__tests__/useFile.test.ts
🧠 Learnings (4)
📚 Learning: 2026-04-30T17:07:51.020Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7274
File: app/lib/services/voip/MediaCallEvents.ts:0-0
Timestamp: 2026-04-30T17:07:51.020Z
Learning: In this Rocket.Chat React Native codebase, the ESLint rule `no-void: error` is enforced. When you see a promise returned from an async call that is not awaited (a “floating promise”), do not silence it with the `void somePromise()` pattern. Instead, handle the promise explicitly by attaching `.catch(...)` (or otherwise awaiting/handling the error) so unhandled-rejection risks are addressed in a way that satisfies the existing ESLint configuration.

Applied to files:

  • app/containers/message/hooks/useMediaAutoDownload.tsx
  • app/views/AttachmentView.tsx
  • app/lib/methods/handleMediaDownload.ts
  • app/containers/message/hooks/useFile.tsx
  • app/lib/methods/helpers/formatAttachmentUrl.ts
  • app/containers/message/hooks/__tests__/useFile.test.ts
📚 Learning: 2026-06-25T18:37:44.793Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7434
File: app/views/ScreenLockConfigView.tsx:101-141
Timestamp: 2026-06-25T18:37:44.793Z
Learning: In the Rocket.Chat React Native codebase, do not treat passing an `async` function directly to an event prop in React/React Native UI components (e.g., `onPress={async () => ...}` in TSX) as a “floating promises” CI-blocking lint issue—this repo does not enable the ESLint `no-floating-promises` rule (while `no-void` is enforced). Only raise robustness follow-ups when there are genuinely unhandled promise paths (e.g., fire-and-forget calls like `save()` that return a Promise that is neither awaited nor handled), and prefer making sure failure paths are explicitly handled/reported rather than blocking on lint-style floating-promise concerns.

Applied to files:

  • app/containers/message/hooks/useMediaAutoDownload.tsx
  • app/views/AttachmentView.tsx
  • app/containers/message/hooks/useFile.tsx
📚 Learning: 2026-06-24T22:58:43.390Z
Learnt from: Rohit3523
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7157
File: app/views/MessagesView/index.tsx:392-392
Timestamp: 2026-06-24T22:58:43.390Z
Learning: When wrapping a React Native component (e.g., via `withSafeAreaInsets`) ensure `hoistNonReactStatics` is only required if the wrapped component actually defines static properties/methods that consumers rely on. If the component has no statics (as in `app/views/MessagesView/index.tsx`), you can omit `hoistNonReactStatics` for this case.

Applied to files:

  • app/views/AttachmentView.tsx
📚 Learning: 2026-06-25T18:37:25.526Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7434
File: app/views/ScreenLockConfigView.test.tsx:16-22
Timestamp: 2026-06-25T18:37:25.526Z
Learning: In Rocket.Chat ReactNative tests that mock selectors for `useAppSelector`, don’t require the mocked selector input to be typed as `IApplicationState` when the fixture only includes a partial Redux state slice (e.g., only `server` and `settings`). Requiring the full `IApplicationState` type in that scenario forces unsafe `as IApplicationState` casts and undermines type-safety. For these narrowly scoped selector-mock fixtures, use a less strict type (e.g., `any`) to keep the mock focused on the slice under test.

Applied to files:

  • app/containers/message/hooks/__tests__/useFile.test.ts
🔇 Additional comments (5)
app/lib/methods/handleMediaDownload.ts (1)

252-262: Run the required TypeScript checks before merging.

Run pnpm prettier-lint and TZ=UTC pnpm test for the modified files, as required by the repository guidelines. As per coding guidelines, these checks are required for JavaScript and TypeScript changes.

Source: Coding guidelines

app/containers/message/hooks/useFile.tsx (1)

16-26: 🎯 Functional Correctness

Verify overrides can't leak across a genuinely different attachment, not just a cosmetic file prop update.

The hook now merges overrides unconditionally and never resets them, which the tests confirm is intentional when the same attachment's metadata changes (e.g., a title edit) — the local download URI should still win. But there's no reset keyed off the attachment's own identity (e.g. title_link/video_url/image_url), so if the same component instance ever renders a different underlying attachment (e.g. a message edit that swaps the attached video while the message id/component instance is unchanged), the stale local override from the previous download would still be applied on top of the new, unrelated remote url.

Can you confirm whether useFile's call sites are always remounted (not reused) whenever the underlying attachment actually changes content, as opposed to just metadata? If reuse across different attachments is possible, consider resetting overrides when a stable identity field of file changes.

app/containers/message/hooks/__tests__/useFile.test.ts (1)

6-46: LGTM!

app/containers/message/hooks/useMediaAutoDownload.tsx (1)

85-85: LGTM!

app/views/AttachmentView.tsx (1)

62-64: LGTM!

Also applies to: 77-77

Comment thread app/lib/methods/handleMediaDownload.ts
@github-actions

Copy link
Copy Markdown

iOS Build Available

Rocket.Chat 4.75.0.109454

@OtavioStasiak OtavioStasiak changed the title fix: video attachments stuck loading forever on iOS fix: video attachments not reproducing on iOS Jul 29, 2026
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

iOS Build Available

Rocket.Chat 4.75.0.109458

Comment thread app/lib/methods/helpers/formatAttachmentUrl.ts
Comment thread app/lib/methods/helpers/formatAttachmentUrl.ts
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

iOS Build Available

Rocket.Chat 4.76.0.109502

@Rohit3523

Rohit3523 commented Aug 6, 2026

Copy link
Copy Markdown
Member

Code review

The encodeAttachmentUrl helper is the right primitive — new URL(x).toString() is idempotent where encodeURI isn't, so the reserved-character hazard from the earlier round is gone. Two issues with how it's wired up.

Found 2 issues:

1. Inline message images still double-encode, and this PR breaks the one config that worked

AttachmentView drops its encodeURI, but the in-message image renderer still has one, fed directly from formatAttachmentUrl:

<View style={[containerStyle, borderStyle]}>
<Image autoplay={autoplayGifs} style={imageStyle} source={{ uri: encodeURI(uri) }} contentFit='cover' />
</View>

The uri comes from useMediaAutoDownload().url, unmodified:

<WidthAwareView>
<MessageImage uri={url} status={status} encrypted={isEncrypted} imagePreview={imagePreview} imageType={imageType} />
</WidthAwareView>

For Screen Recording.png with title_link = /file-upload/abc/Screen Recording.png, which falls through to line 51:

FileUpload_ProtectFiles formatAttachmentUrl after encodeURI at Image.tsx:72
false develop …/Screen Recording.png (raw) Screen%20Recording.png
false this PR …/Screen%20Recording.png Screen%2520Recording.png
true develop …/Screen%20Recording.png Screen%2520Recording.png
true this PR …/Screen%20Recording.png Screen%2520Recording.png

With protectFiles off, that row goes from working to 404. With it on it was already broken and stays broken — so as it stands, tapping through to the fullscreen viewer works but the inline thumbnail doesn't. Videos are unaffected; Image.tsx:72 is the only remaining encodeURI in an attachment render path.

2. _originalUrl bypasses the new encoding, so encodeURI can't simply be deleted

if (attachmentUrl && attachmentUrl.startsWith('http')) {
if (_originalUrl && !_originalUrl.startsWith(server)) {
return _originalUrl;
}

useMediaAutoDownload is the only caller that passes _originalUrl, so externally-hosted attachments return raw — and today Image.tsx:72 is the only thing encoding them. Wrapping that return makes the "always encoded" contract asserted in the new AttachmentView comment actually hold, and then #1 is a one-line deletion:

if (_originalUrl && !_originalUrl.startsWith(server)) {
    return encodeAttachmentUrl(_originalUrl);
}

The file:// and base64 early returns can stay unencoded — sanitizeFileName rewrites %20 to _20, so cached paths never contain spaces.

Minor: the malformed-escape test asserts correct behavior, but not for the stated reason — WHATWG URL doesn't throw on %ZZ, it passes it through, so the catch is never reached. The branch that does throw is a non-absolute URL (new URL('/file-upload/1/x.png')), reachable at line 51 when baseUrl is empty. Worth one more case so the fallback is actually covered.

Comment thread app/containers/message/hooks/useFile.tsx Outdated
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

iOS Build Available

Rocket.Chat 4.76.0.109503

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants