Skip to content

fix: frequently used emoji crash on non-ASCII record ids#7378

Open
diegolmello wants to merge 3 commits into
developfrom
charming-stem
Open

fix: frequently used emoji crash on non-ASCII record ids#7378
diegolmello wants to merge 3 commits into
developfrom
charming-stem

Conversation

@diegolmello

@diegolmello diegolmello commented Jun 2, 2026

Copy link
Copy Markdown
Member

Proposed changes

addFrequentlyUsed used the emoji content / custom-emoji name as the WatermelonDB record id. Custom emoji names are server-controlled and can be non-ASCII (CJK, ZWJ sequences). Non-ASCII ids do not round-trip across the native SQLite/JSI bridge: the adapter's cached-id set desyncs from the JS RecordCache, so the entire frequently_used_emojis .fetch() rejects with Record ID ... was sent over the bridge, but it's not cached and crashes every emoji surface (picker, composer search, reaction bar). Because results map synchronously, a single bad row takes down the whole fetch.

This PR:

  • Lets WatermelonDB own a safe ASCII record id and dedups frequently-used emoji by the content + is_custom fields instead of find-by-id (content stays a plain column value, which round-trips fine).
  • Runs that dedupe lookup inside db.write() so the find-or-create is serialized — two concurrent addFrequentlyUsed calls can no longer both miss the row and insert a duplicate.
  • Centralizes the read in a resilient getFrequentlyUsedEmojis that logs and degrades to defaults on a bridge error instead of crashing the UI.
  • Adds a v29 migration that purges only legacy rows whose id contains a non-printable-ASCII character (id GLOB '*[^ -~]*'), so ASCII shortname ids such as heart_eyes are preserved.
  • Adds unit tests (mocked database.active, the repo's existing DB-test pattern) for the method and the useFrequentlyUsedEmoji hook.

For contrast, getCustomEmojis was never affected — it correctly keys custom_emojis by the server _id (ASCII-safe).

Issue(s)

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

How to test or reproduce

This crash only manifests with the native SQLite/JSI adapter — it cannot be reproduced in pure JS (non-ASCII ids round-trip fine there), so it needs a device/emulator build.

About the trigger: the crash requires a custom emoji whose name is non-ASCII (CJK, Cyrillic, emoji glyphs, etc.). Modern Rocket.Chat servers slugify custom emoji names to ASCII on create (大丈夫da4_zhang4_fu1), so you cannot create a triggering emoji through Admin → Emoji there. To reproduce, use a server that stores non-ASCII names verbatim — an older Rocket.Chat, or any workspace that already carries such an emoji as legacy data.

Reproduce the crash (pre-fix build, non-slugifying server)

  1. On a server that has a custom emoji with a non-ASCII name (e.g. 大丈夫), open any channel.
  2. Open the emoji picker (or long-press a message → reaction bar) and tap that custom emoji — this writes it to frequently_used_emojis.
  3. Reopen any emoji surface (picker / composer emoji search / message reaction bar). Before the fix, the fetch rejects with Record ID frequently_used_emojis#大丈夫 was sent over the bridge, but it's not cached and every emoji surface fails to load.

Verify the fix (this branch, same server)

  1. React with / pick the non-ASCII custom emoji. ✅ No crash; the reaction is added.
  2. Reopen the picker, composer emoji search, and reaction bar. ✅ "Frequently used" shows the emoji and renders it; no crash.
  3. React with it again on another message. ✅ A single frequently-used entry — the count increments, no duplicate row.
  4. ✅ It renders correctly in the message body (:name:), the reaction badge, the picker grid, and the frequently-used row.

Verify the upgrade heals existing installs

  1. On the previous (pre-fix) build, react with the non-ASCII custom emoji to write the corrupt row.
  2. Update to this build and open the app.
  3. ✅ App opens normally, the emoji surfaces work, and the corrupted entry is gone — the v29 migration purges only non-printable-ASCII ids (ASCII shortnames such as heart_eyes are kept).

Regression check (any server)

  1. React with / pick a few standard emoji (e.g. 😀) and an ASCII-named custom emoji.
  2. ✅ Both appear in Frequently used, dedupe and increment count on repeat, and survive an app restart.

Automated coverage

TZ=UTC pnpm test --testPathPattern='app/lib/(methods/emojis|hooks/useFrequentlyUsedEmoji).test'

  • addFrequentlyUsed creates via a content + is_custom query, never sets the emoji as the id, increments count on repeat, and runs its lookup inside the serialized write (no read-before-write race).
  • getFrequentlyUsedEmojis maps row shapes and returns [] / defaults instead of throwing when a bridge desync is simulated.
  • useFrequentlyUsedEmoji exposes the fetched emojis and flips loaded, and refetches when withDefaultEmojis changes.
  • v29 migration deletes only non-printable-ASCII ids.

Screenshots

N/A — crash fix, no visual change.

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

The migration heals existing broken installs once at upgrade; the safe-id change prevents new bad rows; the resilient read is belt-and-suspenders so any unforeseen desync can never crash the emoji UI again.

Out of scope (flagged for follow-up): the same content-as-id anti-pattern exists, lower-risk, in getSlashCommands (command.command) and sendFileMessage (uploadPath, can contain non-ASCII filename chars). Every other sanitizedRaw({ id }) site uses a server _id / rid, which is ASCII-safe.

Summary by CodeRabbit

  • New Features

    • Improved frequently-used emoji handling: normalized lookup, correct counting, deduplication, and optional default fallbacks via a new retrieval API.
  • Bug Fixes

    • Migration cleans up legacy/corrupted emoji records.
    • More robust error handling when emoji data is unavailable.
  • Tests

    • Added tests for emoji management, hook behavior, and the migration cleanup.

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Increments DB schema to v29 and adds a migration that deletes legacy non-ASCII-ID emoji rows; refactors frequently-used emoji storage to use (content, is_custom) with exported getFrequentlyUsedEmojis; updates useFrequentlyUsedEmoji to call the helper; adds Jest tests for methods, hook, and migration.

Changes

Emoji ID Corruption Fix

Layer / File(s) Summary
Schema version and data cleanup migration
app/lib/database/schema/app.js, app/lib/database/model/migrations.js
Schema version incremented from 28 to 29; new migration adds raw SQL DELETE targeting frequently_used_emojis rows whose id contains non-printable ASCII characters; migrations import now includes unsafeExecuteSql.
Stable emoji tracking and retrieval methods
app/lib/methods/emojis.ts
addFrequentlyUsed now queries the frequently-used table by content and is_custom instead of using legacy record IDs; creates/updates records with explicit content, isCustom, extension (for custom), and count. Adds exported getFrequentlyUsedEmojis(withDefaultEmojis) which returns mapped IEmoji[], optionally appending deduped defaults, and handles fetch errors by returning defaults or [].
Hook integration
app/lib/hooks/useFrequentlyUsedEmoji.ts
Hook now calls getFrequentlyUsedEmojis(withDefaultEmojis) and re-runs when withDefaultEmojis changes instead of performing an inline DB query.
Test coverage
app/lib/hooks/useFrequentlyUsedEmoji.test.ts, app/lib/methods/emojis.test.ts
Jest tests added for addFrequentlyUsed (creation without content-derived ID, dedup/upsert behavior, custom vs standard handling), getFrequentlyUsedEmojis (mapping, native-bridge error resilience, defaults fallback), hook behavior (loading and refetch on param change), and v29 migration SQL validation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

type: bug

Suggested reviewers

  • OtavioStasiak
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: addressing a crash caused by non-ASCII record IDs in frequently used emoji tracking. It directly reflects the primary change across all modified files.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • NATIVE-1192: 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 and usage tips.

@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 (1)
app/lib/hooks/useFrequentlyUsedEmoji.ts (1)

14-21: ⚡ Quick win

Handle the effect's async work explicitly.

Line 20 starts an async function and drops the returned promise, and this effect has no cleanup guard if withDefaultEmojis changes or the component unmounts mid-fetch. Wrap the await in try/catch, handle the promise explicitly from the effect entrypoint, and skip setFrequentlyUsed/setLoaded once the effect is stale.

Suggested change
 	useEffect(() => {
+		let cancelled = false;
 		const fetchFrequentlyUsedEmojis = async () => {
-			const emojis = await getFrequentlyUsedEmojis(withDefaultEmojis);
-			setFrequentlyUsed(emojis);
-			setLoaded(true);
+			try {
+				const emojis = await getFrequentlyUsedEmojis(withDefaultEmojis);
+				if (cancelled) {
+					return;
+				}
+				setFrequentlyUsed(emojis);
+				setLoaded(true);
+			} catch {
+				if (!cancelled) {
+					setLoaded(true);
+				}
+			}
 		};
-		fetchFrequentlyUsedEmojis();
+		fetchFrequentlyUsedEmojis().catch(() => undefined);
+		return () => {
+			cancelled = true;
+		};
 	}, [withDefaultEmojis]);

Based on learnings, the repo enforces no-void: error, and as per coding guidelines async operations should use explicit error handling with try/catch blocks.

🤖 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/hooks/useFrequentlyUsedEmoji.ts` around lines 14 - 21, The effect in
useFrequentlyUsedEmoji (useEffect -> fetchFrequentlyUsedEmojis) starts an async
task and drops the promise without error handling or a stale-check; change the
effect so it calls the async work explicitly (do not "void" the promise), wrap
the await getFrequentlyUsedEmojis(withDefaultEmojis) in try/catch, and add a
cancellation guard (e.g., a mounted/aborted flag or AbortController) in the
effect cleanup so that setFrequentlyUsed and setLoaded are skipped if the effect
became stale or the component unmounted; keep references to the existing symbols
fetchFrequentlyUsedEmojis, useEffect, getFrequentlyUsedEmojis,
setFrequentlyUsed, setLoaded, and withDefaultEmojis when making the change.
🤖 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/emojis.ts`:
- Around line 27-49: The lookup for an existing frequently-used emoji is done
before entering the serialized db.write(), allowing a race that creates
duplicate rows; move the query into the db.write() closure so the
check-and-create/update happens atomically under the same serialized transaction
(i.e., inside db.write() perform freqEmojiCollection.query(...).fetch(), inspect
the resulting existing record, and then either update its count or create a new
record), referencing the existing symbols freqEmojiCollection, addFrequentlyUsed
(the caller), db.write, and the existing variable; also add a regression test
that calls addFrequentlyUsed twice in parallel and asserts a single row exists
with count === 2.

---

Nitpick comments:
In `@app/lib/hooks/useFrequentlyUsedEmoji.ts`:
- Around line 14-21: The effect in useFrequentlyUsedEmoji (useEffect ->
fetchFrequentlyUsedEmojis) starts an async task and drops the promise without
error handling or a stale-check; change the effect so it calls the async work
explicitly (do not "void" the promise), wrap the await
getFrequentlyUsedEmojis(withDefaultEmojis) in try/catch, and add a cancellation
guard (e.g., a mounted/aborted flag or AbortController) in the effect cleanup so
that setFrequentlyUsed and setLoaded are skipped if the effect became stale or
the component unmounted; keep references to the existing symbols
fetchFrequentlyUsedEmojis, useEffect, getFrequentlyUsedEmojis,
setFrequentlyUsed, setLoaded, and withDefaultEmojis when making the change.
🪄 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

Run ID: eb156926-4217-4b71-9b78-ca5e5dc02a72

📥 Commits

Reviewing files that changed from the base of the PR and between bbc25d0 and 20b7918.

📒 Files selected for processing (5)
  • app/lib/database/model/migrations.js
  • app/lib/database/schema/app.js
  • app/lib/hooks/useFrequentlyUsedEmoji.ts
  • app/lib/methods/emojis.test.ts
  • app/lib/methods/emojis.ts
📜 Review details
🧰 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/lib/database/schema/app.js
  • app/lib/methods/emojis.test.ts
  • app/lib/methods/emojis.ts
  • app/lib/hooks/useFrequentlyUsedEmoji.ts
  • app/lib/database/model/migrations.js
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use Prettier with tabs, single quotes, 130 char width, no trailing commas, arrow parens avoid, bracket same line
Use @rocket.chat/eslint-config base with React, React Native, TypeScript, Jest plugins

Files:

  • app/lib/database/schema/app.js
  • app/lib/methods/emojis.test.ts
  • app/lib/methods/emojis.ts
  • app/lib/hooks/useFrequentlyUsedEmoji.ts
  • app/lib/database/model/migrations.js
**/*.{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

Use TypeScript with strict mode and baseUrl set to app/ for import resolution

Files:

  • app/lib/methods/emojis.test.ts
  • app/lib/methods/emojis.ts
  • app/lib/hooks/useFrequentlyUsedEmoji.ts
🧠 Learnings (1)
📚 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/lib/methods/emojis.test.ts
  • app/lib/methods/emojis.ts
  • app/lib/hooks/useFrequentlyUsedEmoji.ts
🔇 Additional comments (4)
app/lib/database/schema/app.js (1)

4-4: LGTM!

app/lib/database/model/migrations.js (1)

1-1: LGTM!

Also applies to: 349-358

app/lib/methods/emojis.ts (1)

55-80: LGTM!

app/lib/methods/emojis.test.ts (1)

1-129: LGTM!

Comment thread app/lib/methods/emojis.ts Outdated
@diegolmello diegolmello changed the title fix: frequently used emoji crash on non-ASCII record ids (NATIVE-1192) fix: frequently used emoji crash on non-ASCII record ids Jun 2, 2026
addFrequentlyUsed used the emoji content / custom-emoji name as the
WatermelonDB record id. Custom emoji names are server-controlled and can
be non-ASCII (CJK, ZWJ sequences). Non-ASCII ids do not round-trip across
the native SQLite/JSI bridge: the adapter's cached-id set desyncs from the
JS RecordCache, so the entire frequently_used_emojis .fetch() throws
"Record ID ... was sent over the bridge, but it's not cached" and crashes
every emoji surface (picker, composer search, reaction bar).

- Let WatermelonDB own a safe ASCII record id; dedup by the content +
  is_custom fields instead of find-by-id.
- Centralize the read in a resilient getFrequentlyUsedEmojis that logs and
  degrades to defaults on a bridge error rather than crashing the UI.
- Add a v29 migration purging legacy rows whose id contains a
  non-printable-ASCII character (keeps ASCII shortname ids like heart_eyes).
- Cover with mocked-database.active unit tests.
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.

1 participant