From be81583095b6ae2babdc4ab68f43880808eb7278 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Sat, 10 Jan 2026 01:53:35 +0500 Subject: [PATCH 1/9] Fix Safari emoji corruption bug by inserting ZWNJ between digits/symbols (#, *) and emojis --- src/CONST/index.ts | 4 ++-- src/libs/EmojiUtils.tsx | 18 +++++++++++------- tests/unit/EmojiTest.ts | 42 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 35d3407d9146..aff994c8b03b 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3784,8 +3784,8 @@ const CONST = { ONLY_PRIVATE_USER_AREA: /^[\uE000-\uF8FF\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]+$/u, - // Regex pattern to match a digit followed by an emoji (used for Safari ZWNJ insertion) - DIGIT_FOLLOWED_BY_EMOJI: /(\d)([\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F9FF}\u2600-\u27BF])/gu, + // Regex pattern to match a digit (#, *, or 0-9) followed by an emoji (used for Safari ZWNJ insertion) + DIGIT_OR_SYMBOL_FOLLOWED_BY_EMOJI: /([\d#*])([\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F9FF}\u2600-\u27BF])/gu, TAX_ID: /^\d{9}$/, NON_NUMERIC: /\D/g, diff --git a/src/libs/EmojiUtils.tsx b/src/libs/EmojiUtils.tsx index 4dca2928b6fc..9fd5f7c09b90 100644 --- a/src/libs/EmojiUtils.tsx +++ b/src/libs/EmojiUtils.tsx @@ -664,23 +664,27 @@ function containsOnlyCustomEmoji(text?: string): boolean { } /** - * Insert ZWNJ (Zero-Width Non-Joiner) between digits and emojis to prevent Safari's automatic keycap sequence bug. + * Insert ZWNJ (Zero-Width Non-Joiner) between digits/symbols and emojis to prevent Safari's automatic keycap sequence bug. * - * Safari has a browser-specific behavior where it automatically converts a digit immediately followed by an emoji - * into a Unicode keycap sequence (e.g., "1" + "😄" becomes "1️⃣"). This happens at the browser's input handling level - * before React can process the text, causing character corruption or unexpected joining. + * Safari has a browser-specific behavior where it automatically converts a digit (#, *, or 0-9) immediately followed by an emoji + * into a Unicode keycap sequence (e.g., "1" + "😄" becomes "1️⃣", "#" + "😄" becomes "#️⃣", "*" + "😄" becomes "*️⃣"). + * This happens at the browser's input handling level before React can process the text, causing character corruption or unexpected joining. * * The ZWNJ character (U+200C) is a non-printing Unicode character that prevents the formation of ligatures or - * unwanted character joining. By inserting it between digits and emojis, we break Safari's automatic keycap + * unwanted character joining. By inserting it between digits/symbols and emojis, we break Safari's automatic keycap * sequence detection, ensuring the text displays correctly. * - * Example: "234😄" becomes "234\u200C😄" (ZWNJ is invisible but prevents Safari's corruption) + * Examples: + * - "234😄" becomes "234\u200C😄" + * - "#😄" becomes "#\u200C😄" + * - "*😄" becomes "*\u200C😄" + * (ZWNJ is invisible but prevents Safari's corruption) */ function insertZWNJBetweenDigitAndEmoji(input: string): string { if (!isSafari()) { return input; } - return input.replaceAll(CONST.REGEX.DIGIT_FOLLOWED_BY_EMOJI, '$1\u200C$2'); + return input.replaceAll(CONST.REGEX.DIGIT_OR_SYMBOL_FOLLOWED_BY_EMOJI, '$1\u200C$2'); } /** diff --git a/tests/unit/EmojiTest.ts b/tests/unit/EmojiTest.ts index 203eb12ef52d..4042014cb9a2 100644 --- a/tests/unit/EmojiTest.ts +++ b/tests/unit/EmojiTest.ts @@ -432,6 +432,48 @@ describe('EmojiTest', () => { expect(result.length).toBe(afterEmojiConversion.length + 1); }); + it('should insert ZWNJ between hash symbol (#) and emoji', () => { + // Given a hash symbol immediately followed by an emoji + const input = '#😄'; + // When we process it with insertZWNJBetweenDigitAndEmoji + const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); + // Then ZWNJ should be inserted between the hash and emoji + expect(result).toBe(`#${ZWNJ}😄`); + }); + + it('should insert ZWNJ between asterisk symbol (*) and emoji', () => { + // Given an asterisk symbol immediately followed by an emoji + const input = '*😄'; + // When we process it with insertZWNJBetweenDigitAndEmoji + const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); + // Then ZWNJ should be inserted between the asterisk and emoji + expect(result).toBe(`*${ZWNJ}😄`); + }); + + it('should handle mixed digits and symbols (#, *) followed by emojis', () => { + // Given a string with digits, hash, and asterisk followed by emojis + const input = '1😄 #🚀 *👍'; + // When we process it with insertZWNJBetweenDigitAndEmoji + const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); + // Then ZWNJ should be inserted for each digit/symbol-emoji pair + expect(result).toBe(`1${ZWNJ}😄 #${ZWNJ}🚀 *${ZWNJ}👍`); + }); + + it('should handle consecutive symbol-emoji pairs (# and *)', () => { + // Given consecutive symbol-emoji pairs + const input = '#😄*🚀'; + // When we process it with insertZWNJBetweenDigitAndEmoji + const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); + // Then ZWNJ should be inserted for each pair + expect(result).toBe(`#${ZWNJ}😄*${ZWNJ}🚀`); + }); + + it('should not modify text with space between symbol (# or *) and emoji', () => { + // Given a symbol followed by a space and then an emoji + expect(EmojiUtils.insertZWNJBetweenDigitAndEmoji('# 😄')).toBe('# 😄'); + expect(EmojiUtils.insertZWNJBetweenDigitAndEmoji('* 😄')).toBe('* 😄'); + }); + it('should return input unchanged on non-Safari browsers', () => { // Given we're not on Safari jest.spyOn(Browser, 'isSafari').mockReturnValue(false); From 9061f0a9dc953d4500ed8219b994d6a483e13651 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Sat, 10 Jan 2026 04:03:11 +0500 Subject: [PATCH 2/9] Fix Safari emoji corruption by ensuring two spaces before * and # when they follow emojis --- src/CONST/index.ts | 2 + src/libs/EmojiUtils.tsx | 23 ++++------- tests/unit/EmojiTest.ts | 84 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 15 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index aff994c8b03b..b8d8fad768b8 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3786,6 +3786,8 @@ const CONST = { // Regex pattern to match a digit (#, *, or 0-9) followed by an emoji (used for Safari ZWNJ insertion) DIGIT_OR_SYMBOL_FOLLOWED_BY_EMOJI: /([\d#*])([\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F9FF}\u2600-\u27BF])/gu, + // Regex pattern to match an emoji followed by # or * (prevents Safari corruption when * comes after emoji) + EMOJI_FOLLOWED_BY_SYMBOL: /([\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F9FF}\u2600-\u27BF])([#*])/gu, TAX_ID: /^\d{9}$/, NON_NUMERIC: /\D/g, diff --git a/src/libs/EmojiUtils.tsx b/src/libs/EmojiUtils.tsx index 9fd5f7c09b90..25e44d1fec5e 100644 --- a/src/libs/EmojiUtils.tsx +++ b/src/libs/EmojiUtils.tsx @@ -665,26 +665,19 @@ function containsOnlyCustomEmoji(text?: string): boolean { /** * Insert ZWNJ (Zero-Width Non-Joiner) between digits/symbols and emojis to prevent Safari's automatic keycap sequence bug. - * - * Safari has a browser-specific behavior where it automatically converts a digit (#, *, or 0-9) immediately followed by an emoji - * into a Unicode keycap sequence (e.g., "1" + "😄" becomes "1️⃣", "#" + "😄" becomes "#️⃣", "*" + "😄" becomes "*️⃣"). - * This happens at the browser's input handling level before React can process the text, causing character corruption or unexpected joining. - * - * The ZWNJ character (U+200C) is a non-printing Unicode character that prevents the formation of ligatures or - * unwanted character joining. By inserting it between digits/symbols and emojis, we break Safari's automatic keycap - * sequence detection, ensuring the text displays correctly. - * - * Examples: - * - "234😄" becomes "234\u200C😄" - * - "#😄" becomes "#\u200C😄" - * - "*😄" becomes "*\u200C😄" - * (ZWNJ is invisible but prevents Safari's corruption) + * Safari converts digits/symbols (#, *, 0-9) immediately followed by emojis into Unicode keycap sequences (e.g., "*😄" → "*️⃣"). + * Adding two spaces before * and # when they come after emojis prevents corruption. */ function insertZWNJBetweenDigitAndEmoji(input: string): string { if (!isSafari()) { return input; } - return input.replaceAll(CONST.REGEX.DIGIT_OR_SYMBOL_FOLLOWED_BY_EMOJI, '$1\u200C$2'); + + let result = input.replaceAll(/([\d#*])\uFE0F?\u20E3/gu, '$1'); + result = result.replaceAll(/([\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F9FF}\u2600-\u27BF]) ?([#*])/gu, '$1 $2'); + result = result.replaceAll(CONST.REGEX.DIGIT_OR_SYMBOL_FOLLOWED_BY_EMOJI, '$1\u200C$2'); + + return result; } /** diff --git a/tests/unit/EmojiTest.ts b/tests/unit/EmojiTest.ts index 4042014cb9a2..dba4c3e7551e 100644 --- a/tests/unit/EmojiTest.ts +++ b/tests/unit/EmojiTest.ts @@ -484,5 +484,89 @@ describe('EmojiTest', () => { expect(result).toBe('234😄'); expect(result.includes(ZWNJ)).toBe(false); }); + + it('should add two spaces before asterisk when asterisk comes after emoji', () => { + // Given an emoji followed by asterisk (common mobile Safari issue) + // Scenario: User types "#😄" then "*" - Safari may corrupt the * + const input = '#😄*'; + // When we process it + const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); + // Then two spaces should be added before asterisk, and ZWNJ between # and emoji + expect(result).toBe(`#${ZWNJ}😄 *`); + }); + + it('should handle emoji followed by asterisk then emoji', () => { + // Given emoji, asterisk, then emoji (like "#😄*😀") + const input = '#😄*😀'; + // When we process it + const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); + // Then two spaces should be added before *, and ZWNJ between # and first emoji, and between * and second emoji + expect(result).toBe(`#${ZWNJ}😄 *${ZWNJ}😀`); + }); + + it('should add two spaces before hash symbol when hash comes after emoji', () => { + // Given an emoji followed by hash symbol + const input = '😄#'; + // When we process it + const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); + // Then two spaces should be added before hash + expect(result).toBe(`😄 #`); + }); + + it('should fix corrupted keycap sequence followed by emoji', () => { + // Given Safari has created "*️⃣😄" (corrupted keycap + emoji) + // This happens when Safari corrupts "*😄" to "*️⃣😄" before React processes it + const corruptedKeycapWithEmoji = '*\uFE0F\u20E3😄'; // *️⃣😄 + + // When we process it + const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(corruptedKeycapWithEmoji); + + // Then it should be converted to "*\u200C😄" (preserving the emoji) + expect(result).toBe(`*${ZWNJ}😄`); + }); + + it('should handle space between symbol and emoji correctly', () => { + // Given text with space between symbol and emoji (like "#😃 *😄") + const input = '#😃 *😄'; + + // When we process it + const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); + + // Then ZWNJ should be inserted between symbols and emojis, and two spaces before * (even if one space already exists) + expect(result).toBe(`#${ZWNJ}😃 *${ZWNJ}😄`); + }); + + it('should fix corrupted keycap in text with spaces', () => { + // Given text like "#😃 *️⃣😄" where Safari corrupted "*😄" to "*️⃣😄" + const input = '#😃 *\uFE0F\u20E3😄'; + + // When we process it + const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); + + // Then corrupted keycap should be fixed to "*\u200C😄" (keycap removed, ZWNJ added) + expect(result).toBe(`#${ZWNJ}😃 *${ZWNJ}😄`); + }); + + it('should ensure two spaces before symbol even if one space already exists', () => { + // Given emoji followed by one space then symbol (like "#😄 *") + const input = '#😄 *'; + + // When we process it + const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); + + // Then two spaces should be ensured before the symbol + expect(result).toBe(`#${ZWNJ}😄 *`); + }); + + it('should fix corrupted keycap sequences first', () => { + // Given corrupted keycap sequence (like "*️⃣") + const input = '*\uFE0F\u20E3'; + + // When we process it + const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); + + // Then corrupted keycap should be fixed to just the base character + expect(result).toBe('*'); + }); }); }); From 65083795ac5ba768529eb6557708d3f1af726086 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Sat, 10 Jan 2026 04:14:00 +0500 Subject: [PATCH 3/9] Fixed test cases --- tests/unit/EmojiTest.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/unit/EmojiTest.ts b/tests/unit/EmojiTest.ts index dba4c3e7551e..90174dd96411 100644 --- a/tests/unit/EmojiTest.ts +++ b/tests/unit/EmojiTest.ts @@ -455,8 +455,8 @@ describe('EmojiTest', () => { const input = '1😄 #🚀 *👍'; // When we process it with insertZWNJBetweenDigitAndEmoji const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should be inserted for each digit/symbol-emoji pair - expect(result).toBe(`1${ZWNJ}😄 #${ZWNJ}🚀 *${ZWNJ}👍`); + // Then ZWNJ should be inserted for each digit/symbol-emoji pair, and two spaces before # and * when they come after emojis + expect(result).toBe(`1${ZWNJ}😄 #${ZWNJ}🚀 *${ZWNJ}👍`); }); it('should handle consecutive symbol-emoji pairs (# and *)', () => { @@ -464,8 +464,8 @@ describe('EmojiTest', () => { const input = '#😄*🚀'; // When we process it with insertZWNJBetweenDigitAndEmoji const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should be inserted for each pair - expect(result).toBe(`#${ZWNJ}😄*${ZWNJ}🚀`); + // Then ZWNJ should be inserted for each pair, and two spaces before * when it comes after an emoji + expect(result).toBe(`#${ZWNJ}😄 *${ZWNJ}🚀`); }); it('should not modify text with space between symbol (# or *) and emoji', () => { @@ -543,8 +543,8 @@ describe('EmojiTest', () => { // When we process it const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then corrupted keycap should be fixed to "*\u200C😄" (keycap removed, ZWNJ added) - expect(result).toBe(`#${ZWNJ}😃 *${ZWNJ}😄`); + // Then corrupted keycap should be fixed to "*\u200C😄" (keycap removed, ZWNJ added), and two spaces before * when it comes after an emoji + expect(result).toBe(`#${ZWNJ}😃 *${ZWNJ}😄`); }); it('should ensure two spaces before symbol even if one space already exists', () => { From be8d270aefef9cd830e8c4e7f811aab62c658912 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Wed, 14 Jan 2026 01:26:50 +0500 Subject: [PATCH 4/9] Fix Safari emoji corruption: preserve keycap emojis and use single space for emoji+#/* combinations --- src/libs/EmojiUtils.tsx | 10 +++++--- tests/unit/EmojiTest.ts | 55 ++++++++++++++++++++++------------------- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/libs/EmojiUtils.tsx b/src/libs/EmojiUtils.tsx index 25e44d1fec5e..4df1c6c35b23 100644 --- a/src/libs/EmojiUtils.tsx +++ b/src/libs/EmojiUtils.tsx @@ -666,15 +666,19 @@ function containsOnlyCustomEmoji(text?: string): boolean { /** * Insert ZWNJ (Zero-Width Non-Joiner) between digits/symbols and emojis to prevent Safari's automatic keycap sequence bug. * Safari converts digits/symbols (#, *, 0-9) immediately followed by emojis into Unicode keycap sequences (e.g., "*😄" → "*️⃣"). - * Adding two spaces before * and # when they come after emojis prevents corruption. + * Adding a space before * and # when they come after emojis prevents corruption. */ function insertZWNJBetweenDigitAndEmoji(input: string): string { if (!isSafari()) { return input; } - let result = input.replaceAll(/([\d#*])\uFE0F?\u20E3/gu, '$1'); - result = result.replaceAll(/([\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F9FF}\u2600-\u27BF]) ?([#*])/gu, '$1 $2'); + // Fix corrupted keycaps that Safari created (keycap followed by emoji indicates corruption) + // Only fix keycaps that are immediately followed by another emoji, preserving legitimate standalone keycaps + let result = input.replaceAll(/([\d#*])\uFE0F?\u20E3([\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F9FF}\u2600-\u27BF])/gu, '$1\u200C$2'); + // Ensure exactly one space before * and # when they come after emojis (normalize multiple spaces to one) + result = result.replaceAll(/([\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F9FF}\u2600-\u27BF])\s*([#*])/gu, '$1 $2'); + // Insert ZWNJ between digit/symbol and emoji (the main fix to prevent corruption) result = result.replaceAll(CONST.REGEX.DIGIT_OR_SYMBOL_FOLLOWED_BY_EMOJI, '$1\u200C$2'); return result; diff --git a/tests/unit/EmojiTest.ts b/tests/unit/EmojiTest.ts index 90174dd96411..f6ee82f9225c 100644 --- a/tests/unit/EmojiTest.ts +++ b/tests/unit/EmojiTest.ts @@ -455,8 +455,8 @@ describe('EmojiTest', () => { const input = '1😄 #🚀 *👍'; // When we process it with insertZWNJBetweenDigitAndEmoji const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should be inserted for each digit/symbol-emoji pair, and two spaces before # and * when they come after emojis - expect(result).toBe(`1${ZWNJ}😄 #${ZWNJ}🚀 *${ZWNJ}👍`); + // Then ZWNJ should be inserted for each digit/symbol-emoji pair, and one space before # and * when they come after emojis + expect(result).toBe(`1${ZWNJ}😄 #${ZWNJ}🚀 *${ZWNJ}👍`); }); it('should handle consecutive symbol-emoji pairs (# and *)', () => { @@ -464,8 +464,8 @@ describe('EmojiTest', () => { const input = '#😄*🚀'; // When we process it with insertZWNJBetweenDigitAndEmoji const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should be inserted for each pair, and two spaces before * when it comes after an emoji - expect(result).toBe(`#${ZWNJ}😄 *${ZWNJ}🚀`); + // Then ZWNJ should be inserted for each pair, and one space before * when it comes after an emoji + expect(result).toBe(`#${ZWNJ}😄 *${ZWNJ}🚀`); }); it('should not modify text with space between symbol (# or *) and emoji', () => { @@ -485,14 +485,14 @@ describe('EmojiTest', () => { expect(result.includes(ZWNJ)).toBe(false); }); - it('should add two spaces before asterisk when asterisk comes after emoji', () => { + it('should add one space before asterisk when asterisk comes after emoji', () => { // Given an emoji followed by asterisk (common mobile Safari issue) // Scenario: User types "#😄" then "*" - Safari may corrupt the * const input = '#😄*'; // When we process it const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then two spaces should be added before asterisk, and ZWNJ between # and emoji - expect(result).toBe(`#${ZWNJ}😄 *`); + // Then one space should be added before asterisk, and ZWNJ between # and emoji + expect(result).toBe(`#${ZWNJ}😄 *`); }); it('should handle emoji followed by asterisk then emoji', () => { @@ -500,17 +500,17 @@ describe('EmojiTest', () => { const input = '#😄*😀'; // When we process it const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then two spaces should be added before *, and ZWNJ between # and first emoji, and between * and second emoji - expect(result).toBe(`#${ZWNJ}😄 *${ZWNJ}😀`); + // Then one space should be added before *, and ZWNJ between # and first emoji, and between * and second emoji + expect(result).toBe(`#${ZWNJ}😄 *${ZWNJ}😀`); }); - it('should add two spaces before hash symbol when hash comes after emoji', () => { + it('should add one space before hash symbol when hash comes after emoji', () => { // Given an emoji followed by hash symbol const input = '😄#'; // When we process it const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then two spaces should be added before hash - expect(result).toBe(`😄 #`); + // Then one space should be added before hash + expect(result).toBe(`😄 #`); }); it('should fix corrupted keycap sequence followed by emoji', () => { @@ -532,8 +532,8 @@ describe('EmojiTest', () => { // When we process it const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should be inserted between symbols and emojis, and two spaces before * (even if one space already exists) - expect(result).toBe(`#${ZWNJ}😃 *${ZWNJ}😄`); + // Then ZWNJ should be inserted between symbols and emojis, and one space before * (normalized from existing space) + expect(result).toBe(`#${ZWNJ}😃 *${ZWNJ}😄`); }); it('should fix corrupted keycap in text with spaces', () => { @@ -543,30 +543,33 @@ describe('EmojiTest', () => { // When we process it const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then corrupted keycap should be fixed to "*\u200C😄" (keycap removed, ZWNJ added), and two spaces before * when it comes after an emoji - expect(result).toBe(`#${ZWNJ}😃 *${ZWNJ}😄`); + // Then corrupted keycap should be fixed to "*\u200C😄" (keycap removed, ZWNJ added), and one space before * when it comes after an emoji + expect(result).toBe(`#${ZWNJ}😃 *${ZWNJ}😄`); }); - it('should ensure two spaces before symbol even if one space already exists', () => { - // Given emoji followed by one space then symbol (like "#😄 *") - const input = '#😄 *'; + it('should normalize to one space before symbol even if multiple spaces exist', () => { + // Given emoji followed by one or more spaces then symbol (like "#😄 *" or "#😄 *") + const inputOneSpace = '#😄 *'; + const inputTwoSpaces = '#😄 *'; // When we process it - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); + const resultOneSpace = EmojiUtils.insertZWNJBetweenDigitAndEmoji(inputOneSpace); + const resultTwoSpaces = EmojiUtils.insertZWNJBetweenDigitAndEmoji(inputTwoSpaces); - // Then two spaces should be ensured before the symbol - expect(result).toBe(`#${ZWNJ}😄 *`); + // Then exactly one space should be ensured before the symbol in both cases + expect(resultOneSpace).toBe(`#${ZWNJ}😄 *`); + expect(resultTwoSpaces).toBe(`#${ZWNJ}😄 *`); }); - it('should fix corrupted keycap sequences first', () => { - // Given corrupted keycap sequence (like "*️⃣") + it('should preserve legitimate standalone keycap emojis', () => { + // Given a legitimate standalone keycap emoji (like "*️⃣") const input = '*\uFE0F\u20E3'; // When we process it const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then corrupted keycap should be fixed to just the base character - expect(result).toBe('*'); + // Then the keycap should be preserved (not removed) since it's not followed by another emoji + expect(result).toBe('*\uFE0F\u20E3'); }); }); }); From 4e083cbf725064c928b647f6b5295c4293ab0ad8 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Wed, 14 Jan 2026 01:48:48 +0500 Subject: [PATCH 5/9] Fixed keycaps issue --- src/libs/EmojiUtils.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/EmojiUtils.tsx b/src/libs/EmojiUtils.tsx index 4df1c6c35b23..bd30c38cf333 100644 --- a/src/libs/EmojiUtils.tsx +++ b/src/libs/EmojiUtils.tsx @@ -673,8 +673,8 @@ function insertZWNJBetweenDigitAndEmoji(input: string): string { return input; } - // Fix corrupted keycaps that Safari created (keycap followed by emoji indicates corruption) - // Only fix keycaps that are immediately followed by another emoji, preserving legitimate standalone keycaps + // Fix corrupted key caps that Safari created (key cap followed by emoji indicates corruption) + // Only fix key caps that are immediately followed by another emoji, preserving legitimate standalone key caps let result = input.replaceAll(/([\d#*])\uFE0F?\u20E3([\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F9FF}\u2600-\u27BF])/gu, '$1\u200C$2'); // Ensure exactly one space before * and # when they come after emojis (normalize multiple spaces to one) result = result.replaceAll(/([\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F9FF}\u2600-\u27BF])\s*([#*])/gu, '$1 $2'); From dd56d9474a88b9fd3ff5af88dafe09153d0194a0 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Wed, 21 Jan 2026 19:14:41 +0500 Subject: [PATCH 6/9] Fixed cursor position and spacing issues related to Safari emoji corruption --- src/CONST/index.ts | 4 ++-- src/libs/EmojiUtils.tsx | 10 ++++----- src/libs/Firebase/index.web.ts | 4 ++++ tests/unit/EmojiTest.ts | 40 +++++++++++++++++----------------- 4 files changed, 31 insertions(+), 27 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index bfe29fa480c4..8e6b3f7d5e68 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3812,8 +3812,8 @@ const CONST = { // Regex pattern to match a digit (#, *, or 0-9) followed by an emoji (used for Safari ZWNJ insertion) DIGIT_OR_SYMBOL_FOLLOWED_BY_EMOJI: /([\d#*])([\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F9FF}\u2600-\u27BF])/gu, - // Regex pattern to match an emoji followed by # or * (prevents Safari corruption when * comes after emoji) - EMOJI_FOLLOWED_BY_SYMBOL: /([\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F9FF}\u2600-\u27BF])([#*])/gu, + // Regex pattern to match an emoji followed by optional whitespace and then # or * (prevents Safari corruption when * comes after emoji) + EMOJI_FOLLOWED_BY_SYMBOL: /([\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F9FF}\u2600-\u27BF])\s*([#*])/gu, TAX_ID: /^\d{9}$/, NON_NUMERIC: /\D/g, diff --git a/src/libs/EmojiUtils.tsx b/src/libs/EmojiUtils.tsx index bd30c38cf333..82f529d9e25b 100644 --- a/src/libs/EmojiUtils.tsx +++ b/src/libs/EmojiUtils.tsx @@ -666,7 +666,7 @@ function containsOnlyCustomEmoji(text?: string): boolean { /** * Insert ZWNJ (Zero-Width Non-Joiner) between digits/symbols and emojis to prevent Safari's automatic keycap sequence bug. * Safari converts digits/symbols (#, *, 0-9) immediately followed by emojis into Unicode keycap sequences (e.g., "*😄" → "*️⃣"). - * Adding a space before * and # when they come after emojis prevents corruption. + * Inserting ZWNJ between emoji and symbol, and between symbol and emoji, prevents corruption without breaking markdown formatting. */ function insertZWNJBetweenDigitAndEmoji(input: string): string { if (!isSafari()) { @@ -676,8 +676,8 @@ function insertZWNJBetweenDigitAndEmoji(input: string): string { // Fix corrupted key caps that Safari created (key cap followed by emoji indicates corruption) // Only fix key caps that are immediately followed by another emoji, preserving legitimate standalone key caps let result = input.replaceAll(/([\d#*])\uFE0F?\u20E3([\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F9FF}\u2600-\u27BF])/gu, '$1\u200C$2'); - // Ensure exactly one space before * and # when they come after emojis (normalize multiple spaces to one) - result = result.replaceAll(/([\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F9FF}\u2600-\u27BF])\s*([#*])/gu, '$1 $2'); + // Insert ZWNJ between emoji and symbol to prevent Safari from corrupting the symbol + result = result.replaceAll(/([\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F9FF}\u2600-\u27BF])(\s*)([#*])/gu, '$1$2\u200C$3'); // Insert ZWNJ between digit/symbol and emoji (the main fix to prevent corruption) result = result.replaceAll(CONST.REGEX.DIGIT_OR_SYMBOL_FOLLOWED_BY_EMOJI, '$1\u200C$2'); @@ -685,8 +685,8 @@ function insertZWNJBetweenDigitAndEmoji(input: string): string { } /** - * Calculate the ZWNJ offset for cursor position adjustment. - * Returns the number of ZWNJ characters inserted before the cursor position. + * Calculate the cursor offset for character insertions (ZWNJ) to prevent cursor jumping. + * Returns the number of characters inserted before the cursor position. */ function getZWNJCursorOffset(text: string, cursorPosition: number | undefined | null): number { if (!isSafari() || cursorPosition === undefined || cursorPosition === null) { diff --git a/src/libs/Firebase/index.web.ts b/src/libs/Firebase/index.web.ts index 9cb017551a26..dae697201065 100644 --- a/src/libs/Firebase/index.web.ts +++ b/src/libs/Firebase/index.web.ts @@ -13,6 +13,10 @@ const startTrace: StartTrace = (customEventName) => { return; } + if (!firebasePerfWeb) { + return; + } + if (traceMap[customEventName]) { return; } diff --git a/tests/unit/EmojiTest.ts b/tests/unit/EmojiTest.ts index f6ee82f9225c..43f399f509bc 100644 --- a/tests/unit/EmojiTest.ts +++ b/tests/unit/EmojiTest.ts @@ -455,8 +455,8 @@ describe('EmojiTest', () => { const input = '1😄 #🚀 *👍'; // When we process it with insertZWNJBetweenDigitAndEmoji const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should be inserted for each digit/symbol-emoji pair, and one space before # and * when they come after emojis - expect(result).toBe(`1${ZWNJ}😄 #${ZWNJ}🚀 *${ZWNJ}👍`); + // Then ZWNJ should be inserted for each digit/symbol-emoji pair, and between emoji and symbol + expect(result).toBe(`1${ZWNJ}😄 ${ZWNJ}#${ZWNJ}🚀 ${ZWNJ}*${ZWNJ}👍`); }); it('should handle consecutive symbol-emoji pairs (# and *)', () => { @@ -464,8 +464,8 @@ describe('EmojiTest', () => { const input = '#😄*🚀'; // When we process it with insertZWNJBetweenDigitAndEmoji const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should be inserted for each pair, and one space before * when it comes after an emoji - expect(result).toBe(`#${ZWNJ}😄 *${ZWNJ}🚀`); + // Then ZWNJ should be inserted for each pair, and between emoji and symbol + expect(result).toBe(`#${ZWNJ}😄${ZWNJ}*${ZWNJ}🚀`); }); it('should not modify text with space between symbol (# or *) and emoji', () => { @@ -485,14 +485,14 @@ describe('EmojiTest', () => { expect(result.includes(ZWNJ)).toBe(false); }); - it('should add one space before asterisk when asterisk comes after emoji', () => { + it('should insert ZWNJ between emoji and asterisk when asterisk comes after emoji', () => { // Given an emoji followed by asterisk (common mobile Safari issue) // Scenario: User types "#😄" then "*" - Safari may corrupt the * const input = '#😄*'; // When we process it const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then one space should be added before asterisk, and ZWNJ between # and emoji - expect(result).toBe(`#${ZWNJ}😄 *`); + // Then ZWNJ should be inserted between # and emoji, and between emoji and * + expect(result).toBe(`#${ZWNJ}😄${ZWNJ}*`); }); it('should handle emoji followed by asterisk then emoji', () => { @@ -500,17 +500,17 @@ describe('EmojiTest', () => { const input = '#😄*😀'; // When we process it const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then one space should be added before *, and ZWNJ between # and first emoji, and between * and second emoji - expect(result).toBe(`#${ZWNJ}😄 *${ZWNJ}😀`); + // Then ZWNJ should be inserted between # and first emoji, between emoji and *, and between * and second emoji + expect(result).toBe(`#${ZWNJ}😄${ZWNJ}*${ZWNJ}😀`); }); - it('should add one space before hash symbol when hash comes after emoji', () => { + it('should insert ZWNJ between emoji and hash symbol when hash comes after emoji', () => { // Given an emoji followed by hash symbol const input = '😄#'; // When we process it const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then one space should be added before hash - expect(result).toBe(`😄 #`); + // Then ZWNJ should be inserted between emoji and hash + expect(result).toBe(`😄${ZWNJ}#`); }); it('should fix corrupted keycap sequence followed by emoji', () => { @@ -532,8 +532,8 @@ describe('EmojiTest', () => { // When we process it const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should be inserted between symbols and emojis, and one space before * (normalized from existing space) - expect(result).toBe(`#${ZWNJ}😃 *${ZWNJ}😄`); + // Then ZWNJ should be inserted between symbols and emojis, and between emoji and symbol + expect(result).toBe(`#${ZWNJ}😃 ${ZWNJ}*${ZWNJ}😄`); }); it('should fix corrupted keycap in text with spaces', () => { @@ -543,11 +543,11 @@ describe('EmojiTest', () => { // When we process it const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then corrupted keycap should be fixed to "*\u200C😄" (keycap removed, ZWNJ added), and one space before * when it comes after an emoji - expect(result).toBe(`#${ZWNJ}😃 *${ZWNJ}😄`); + // Then corrupted keycap should be fixed to "*\u200C😄" (keycap removed, ZWNJ added), and ZWNJ between emoji and symbol + expect(result).toBe(`#${ZWNJ}😃 ${ZWNJ}*${ZWNJ}😄`); }); - it('should normalize to one space before symbol even if multiple spaces exist', () => { + it('should preserve spaces and insert ZWNJ between emoji and symbol', () => { // Given emoji followed by one or more spaces then symbol (like "#😄 *" or "#😄 *") const inputOneSpace = '#😄 *'; const inputTwoSpaces = '#😄 *'; @@ -556,9 +556,9 @@ describe('EmojiTest', () => { const resultOneSpace = EmojiUtils.insertZWNJBetweenDigitAndEmoji(inputOneSpace); const resultTwoSpaces = EmojiUtils.insertZWNJBetweenDigitAndEmoji(inputTwoSpaces); - // Then exactly one space should be ensured before the symbol in both cases - expect(resultOneSpace).toBe(`#${ZWNJ}😄 *`); - expect(resultTwoSpaces).toBe(`#${ZWNJ}😄 *`); + // Then ZWNJ should be inserted between emoji and symbol, preserving existing spaces + expect(resultOneSpace).toBe(`#${ZWNJ}😄 ${ZWNJ}*`); + expect(resultTwoSpaces).toBe(`#${ZWNJ}😄 ${ZWNJ}*`); }); it('should preserve legitimate standalone keycap emojis', () => { From b1e54d551f4bddd7916afe9e5567248e19f46ce2 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Fri, 30 Jan 2026 04:25:10 +0500 Subject: [PATCH 7/9] Fixed cursor issue --- src/libs/EmojiUtils.tsx | 4 +--- tests/unit/EmojiTest.ts | 44 ++++++++++++++++++++--------------------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/libs/EmojiUtils.tsx b/src/libs/EmojiUtils.tsx index 82f529d9e25b..ced387fed09e 100644 --- a/src/libs/EmojiUtils.tsx +++ b/src/libs/EmojiUtils.tsx @@ -666,7 +666,7 @@ function containsOnlyCustomEmoji(text?: string): boolean { /** * Insert ZWNJ (Zero-Width Non-Joiner) between digits/symbols and emojis to prevent Safari's automatic keycap sequence bug. * Safari converts digits/symbols (#, *, 0-9) immediately followed by emojis into Unicode keycap sequences (e.g., "*😄" → "*️⃣"). - * Inserting ZWNJ between emoji and symbol, and between symbol and emoji, prevents corruption without breaking markdown formatting. + * Inserting ZWNJ between symbol and emoji prevents corruption without breaking cursor navigation. */ function insertZWNJBetweenDigitAndEmoji(input: string): string { if (!isSafari()) { @@ -676,8 +676,6 @@ function insertZWNJBetweenDigitAndEmoji(input: string): string { // Fix corrupted key caps that Safari created (key cap followed by emoji indicates corruption) // Only fix key caps that are immediately followed by another emoji, preserving legitimate standalone key caps let result = input.replaceAll(/([\d#*])\uFE0F?\u20E3([\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F9FF}\u2600-\u27BF])/gu, '$1\u200C$2'); - // Insert ZWNJ between emoji and symbol to prevent Safari from corrupting the symbol - result = result.replaceAll(/([\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F9FF}\u2600-\u27BF])(\s*)([#*])/gu, '$1$2\u200C$3'); // Insert ZWNJ between digit/symbol and emoji (the main fix to prevent corruption) result = result.replaceAll(CONST.REGEX.DIGIT_OR_SYMBOL_FOLLOWED_BY_EMOJI, '$1\u200C$2'); diff --git a/tests/unit/EmojiTest.ts b/tests/unit/EmojiTest.ts index 43f399f509bc..34e43412839f 100644 --- a/tests/unit/EmojiTest.ts +++ b/tests/unit/EmojiTest.ts @@ -455,8 +455,8 @@ describe('EmojiTest', () => { const input = '1😄 #🚀 *👍'; // When we process it with insertZWNJBetweenDigitAndEmoji const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should be inserted for each digit/symbol-emoji pair, and between emoji and symbol - expect(result).toBe(`1${ZWNJ}😄 ${ZWNJ}#${ZWNJ}🚀 ${ZWNJ}*${ZWNJ}👍`); + // Then ZWNJ should be inserted only between digit/symbol and emoji (not emoji-to-symbol) + expect(result).toBe(`1${ZWNJ}😄 #${ZWNJ}🚀 *${ZWNJ}👍`); }); it('should handle consecutive symbol-emoji pairs (# and *)', () => { @@ -464,8 +464,8 @@ describe('EmojiTest', () => { const input = '#😄*🚀'; // When we process it with insertZWNJBetweenDigitAndEmoji const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should be inserted for each pair, and between emoji and symbol - expect(result).toBe(`#${ZWNJ}😄${ZWNJ}*${ZWNJ}🚀`); + // Then ZWNJ should be inserted only between symbol and emoji (not emoji-to-symbol) + expect(result).toBe(`#${ZWNJ}😄*${ZWNJ}🚀`); }); it('should not modify text with space between symbol (# or *) and emoji', () => { @@ -485,14 +485,14 @@ describe('EmojiTest', () => { expect(result.includes(ZWNJ)).toBe(false); }); - it('should insert ZWNJ between emoji and asterisk when asterisk comes after emoji', () => { - // Given an emoji followed by asterisk (common mobile Safari issue) - // Scenario: User types "#😄" then "*" - Safari may corrupt the * + it('should not insert ZWNJ between emoji and asterisk (to preserve cursor navigation)', () => { + // Given an emoji followed by asterisk + // We don't insert ZWNJ here to avoid cursor navigation issues const input = '#😄*'; // When we process it const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should be inserted between # and emoji, and between emoji and * - expect(result).toBe(`#${ZWNJ}😄${ZWNJ}*`); + // Then ZWNJ should only be inserted between # and emoji (not emoji-to-symbol) + expect(result).toBe(`#${ZWNJ}😄*`); }); it('should handle emoji followed by asterisk then emoji', () => { @@ -500,17 +500,17 @@ describe('EmojiTest', () => { const input = '#😄*😀'; // When we process it const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should be inserted between # and first emoji, between emoji and *, and between * and second emoji - expect(result).toBe(`#${ZWNJ}😄${ZWNJ}*${ZWNJ}😀`); + // Then ZWNJ should be inserted between # and first emoji, and between * and second emoji (not emoji-to-symbol) + expect(result).toBe(`#${ZWNJ}😄*${ZWNJ}😀`); }); - it('should insert ZWNJ between emoji and hash symbol when hash comes after emoji', () => { + it('should not insert ZWNJ between emoji and hash symbol (to preserve cursor navigation)', () => { // Given an emoji followed by hash symbol const input = '😄#'; // When we process it const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should be inserted between emoji and hash - expect(result).toBe(`😄${ZWNJ}#`); + // Then no ZWNJ should be inserted (emoji-to-symbol case is not modified) + expect(result).toBe('😄#'); }); it('should fix corrupted keycap sequence followed by emoji', () => { @@ -532,8 +532,8 @@ describe('EmojiTest', () => { // When we process it const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should be inserted between symbols and emojis, and between emoji and symbol - expect(result).toBe(`#${ZWNJ}😃 ${ZWNJ}*${ZWNJ}😄`); + // Then ZWNJ should only be inserted between symbol and emoji (not emoji-to-symbol) + expect(result).toBe(`#${ZWNJ}😃 *${ZWNJ}😄`); }); it('should fix corrupted keycap in text with spaces', () => { @@ -543,11 +543,11 @@ describe('EmojiTest', () => { // When we process it const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then corrupted keycap should be fixed to "*\u200C😄" (keycap removed, ZWNJ added), and ZWNJ between emoji and symbol - expect(result).toBe(`#${ZWNJ}😃 ${ZWNJ}*${ZWNJ}😄`); + // Then corrupted keycap should be fixed to "*\u200C😄" (keycap removed, ZWNJ added) + expect(result).toBe(`#${ZWNJ}😃 *${ZWNJ}😄`); }); - it('should preserve spaces and insert ZWNJ between emoji and symbol', () => { + it('should not modify emoji followed by spaces and symbol', () => { // Given emoji followed by one or more spaces then symbol (like "#😄 *" or "#😄 *") const inputOneSpace = '#😄 *'; const inputTwoSpaces = '#😄 *'; @@ -556,9 +556,9 @@ describe('EmojiTest', () => { const resultOneSpace = EmojiUtils.insertZWNJBetweenDigitAndEmoji(inputOneSpace); const resultTwoSpaces = EmojiUtils.insertZWNJBetweenDigitAndEmoji(inputTwoSpaces); - // Then ZWNJ should be inserted between emoji and symbol, preserving existing spaces - expect(resultOneSpace).toBe(`#${ZWNJ}😄 ${ZWNJ}*`); - expect(resultTwoSpaces).toBe(`#${ZWNJ}😄 ${ZWNJ}*`); + // Then no ZWNJ should be inserted between emoji and symbol (only between # and emoji) + expect(resultOneSpace).toBe(`#${ZWNJ}😄 *`); + expect(resultTwoSpaces).toBe(`#${ZWNJ}😄 *`); }); it('should preserve legitimate standalone keycap emojis', () => { From 16c092c38943a7fc94b96f1416d25f914d042318 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Tue, 10 Feb 2026 17:00:56 +0500 Subject: [PATCH 8/9] resolved feedbacks --- src/CONST/index.ts | 4 ++-- src/libs/EmojiUtils.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 48dbf698e327..fb9b94194237 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3936,8 +3936,8 @@ const CONST = { // Regex pattern to match a digit (#, *, or 0-9) followed by an emoji (used for Safari ZWNJ insertion) DIGIT_OR_SYMBOL_FOLLOWED_BY_EMOJI: /([\d#*])([\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F9FF}\u2600-\u27BF])/gu, - // Regex pattern to match an emoji followed by optional whitespace and then # or * (prevents Safari corruption when * comes after emoji) - EMOJI_FOLLOWED_BY_SYMBOL: /([\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F9FF}\u2600-\u27BF])\s*([#*])/gu, + // Regex pattern to match a corrupted keycap sequence followed by an emoji + CORRUPTED_KEYCAP_FOLLOWED_BY_EMOJI: /([\d#*])\uFE0F?\u20E3([\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F9FF}\u2600-\u27BF])/gu, TAX_ID: /^\d{9}$/, NON_NUMERIC: /\D/g, diff --git a/src/libs/EmojiUtils.tsx b/src/libs/EmojiUtils.tsx index 118f4a128ae3..51e5da77a84a 100644 --- a/src/libs/EmojiUtils.tsx +++ b/src/libs/EmojiUtils.tsx @@ -766,7 +766,7 @@ function insertZWNJBetweenDigitAndEmoji(input: string): string { // Fix corrupted key caps that Safari created (key cap followed by emoji indicates corruption) // Only fix key caps that are immediately followed by another emoji, preserving legitimate standalone key caps - let result = input.replaceAll(/([\d#*])\uFE0F?\u20E3([\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F9FF}\u2600-\u27BF])/gu, '$1\u200C$2'); + let result = input.replaceAll(CONST.REGEX.CORRUPTED_KEYCAP_FOLLOWED_BY_EMOJI, '$1\u200C$2'); // Insert ZWNJ between digit/symbol and emoji (the main fix to prevent corruption) result = result.replaceAll(CONST.REGEX.DIGIT_OR_SYMBOL_FOLLOWED_BY_EMOJI, '$1\u200C$2'); @@ -783,7 +783,7 @@ function getZWNJCursorOffset(text: string, cursorPosition: number | undefined | } const textBeforeCursor = text.substring(0, cursorPosition); const textWithZWNJBeforeCursor = insertZWNJBetweenDigitAndEmoji(textBeforeCursor); - return textWithZWNJBeforeCursor.length - textBeforeCursor.length; + return Math.max(0, textWithZWNJBeforeCursor.length - textBeforeCursor.length); } export type {HeaderIndices, EmojiPickerList, EmojiPickerListItem}; From e0e947b73e2872c1bf99dbdb2284ec6f8b972a8e Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Tue, 17 Feb 2026 16:08:29 +0500 Subject: [PATCH 9/9] fix: use FE0E instead of ZWNJ to prevent Safari keycap corruption for digits and symbols followed by emojis --- src/CONST/index.ts | 2 +- src/libs/EmojiUtils.tsx | 49 ++-- .../ComposerWithSuggestions.tsx | 8 +- .../report/ReportActionItemMessageEdit.tsx | 8 +- tests/unit/EmojiTest.ts | 248 ++++++------------ 5 files changed, 124 insertions(+), 191 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 7dfef340c8aa..e9ad41514955 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3970,7 +3970,7 @@ const CONST = { ONLY_PRIVATE_USER_AREA: /^[\uE000-\uF8FF\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]+$/u, - // Regex pattern to match a digit (#, *, or 0-9) followed by an emoji (used for Safari ZWNJ insertion) + // Regex pattern to match a digit (#, *, or 0-9) followed by an emoji (used for Safari FE0E insertion to prevent keycap corruption) DIGIT_OR_SYMBOL_FOLLOWED_BY_EMOJI: /([\d#*])([\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F9FF}\u2600-\u27BF])/gu, // Regex pattern to match a corrupted keycap sequence followed by an emoji CORRUPTED_KEYCAP_FOLLOWED_BY_EMOJI: /([\d#*])\uFE0F?\u20E3([\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F9FF}\u2600-\u27BF])/gu, diff --git a/src/libs/EmojiUtils.tsx b/src/libs/EmojiUtils.tsx index 51e5da77a84a..bfe5a67c35d5 100644 --- a/src/libs/EmojiUtils.tsx +++ b/src/libs/EmojiUtils.tsx @@ -755,35 +755,52 @@ function containsOnlyCustomEmoji(text?: string): boolean { } /** - * Insert ZWNJ (Zero-Width Non-Joiner) between digits/symbols and emojis to prevent Safari's automatic keycap sequence bug. - * Safari converts digits/symbols (#, *, 0-9) immediately followed by emojis into Unicode keycap sequences (e.g., "*😄" → "*️⃣"). - * Inserting ZWNJ between symbol and emoji prevents corruption without breaking cursor navigation. + * Insert Variation Selector 15 (FE0E) between digits/symbols and emojis to prevent Safari's automatic keycap sequence bug. + * + * Safari has a browser-specific behavior where it automatically converts a digit or symbol (#, *) + * immediately followed by an emoji into a Unicode keycap sequence (e.g., "1" + "😄" becomes "1️⃣"). + * This happens at the browser's input handling level before React can process the text, + * causing character corruption or unexpected joining. + * + * FE0E (Variation Selector 15) is a non-printing Unicode character that forces text presentation. + * Unlike ZWNJ (U+200C) which has Grapheme_Cluster_Break=Control and creates a separate grapheme cluster + * (causing cursor navigation and deletion issues), FE0E has Grapheme_Cluster_Break=Extend which makes it + * attach to the preceding character as part of the same grapheme cluster. This means: + * - No invisible extra cursor stops between digit and emoji + * - Backspace correctly deletes the digit (not an invisible character) + * - Arrow keys move smoothly past the digit-emoji boundary + * + * Example: "234😄" becomes "234\uFE0E😄" (FE0E is invisible but prevents Safari's corruption) */ -function insertZWNJBetweenDigitAndEmoji(input: string): string { +function insertTextVSBetweenDigitAndEmoji(input: string): string { if (!isSafari()) { return input; } // Fix corrupted key caps that Safari created (key cap followed by emoji indicates corruption) - // Only fix key caps that are immediately followed by another emoji, preserving legitimate standalone key caps - let result = input.replaceAll(CONST.REGEX.CORRUPTED_KEYCAP_FOLLOWED_BY_EMOJI, '$1\u200C$2'); - // Insert ZWNJ between digit/symbol and emoji (the main fix to prevent corruption) - result = result.replaceAll(CONST.REGEX.DIGIT_OR_SYMBOL_FOLLOWED_BY_EMOJI, '$1\u200C$2'); + let result = input.replaceAll(CONST.REGEX.CORRUPTED_KEYCAP_FOLLOWED_BY_EMOJI, '$1\uFE0E$2'); + // Insert FE0E between digit/symbol and emoji (the main fix to prevent corruption) + result = result.replaceAll(CONST.REGEX.DIGIT_OR_SYMBOL_FOLLOWED_BY_EMOJI, '$1\uFE0E$2'); return result; } /** - * Calculate the cursor offset for character insertions (ZWNJ) to prevent cursor jumping. - * Returns the number of characters inserted before the cursor position. + * Calculate the text VS (FE0E) offset for cursor position adjustment. + * Returns the number of FE0E characters that would be inserted before the cursor position. */ -function getZWNJCursorOffset(text: string, cursorPosition: number | undefined | null): number { +function getTextVSCursorOffset(text: string, cursorPosition: number | undefined | null): number { if (!isSafari() || cursorPosition === undefined || cursorPosition === null) { return 0; } - const textBeforeCursor = text.substring(0, cursorPosition); - const textWithZWNJBeforeCursor = insertZWNJBetweenDigitAndEmoji(textBeforeCursor); - return Math.max(0, textWithZWNJBeforeCursor.length - textBeforeCursor.length); + + const beforeCursor = text.substring(0, cursorPosition); + + let processed = beforeCursor; + processed = processed.replaceAll(CONST.REGEX.CORRUPTED_KEYCAP_FOLLOWED_BY_EMOJI, '$1\uFE0E$2'); + processed = processed.replaceAll(CONST.REGEX.DIGIT_OR_SYMBOL_FOLLOWED_BY_EMOJI, '$1\uFE0E$2'); + + return processed.length - beforeCursor.length; } export type {HeaderIndices, EmojiPickerList, EmojiPickerListItem}; @@ -813,8 +830,8 @@ export { containsCustomEmoji, containsOnlyCustomEmoji, processFrequentlyUsedEmojis, - insertZWNJBetweenDigitAndEmoji, - getZWNJCursorOffset, + insertTextVSBetweenDigitAndEmoji, + getTextVSCursorOffset, isPositionInsideCodeBlock, getEmojiCodeForInsertion, }; diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 549bfef79223..b153dc096107 100644 --- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -27,7 +27,7 @@ import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; import {forceClearInput} from '@libs/ComponentUtils'; import {canSkipTriggerHotkeys, findCommonSuffixLength, insertText, insertWhiteSpaceAtIndex} from '@libs/ComposerUtils'; import convertToLTRForComposer from '@libs/convertToLTRForComposer'; -import {containsOnlyEmojis, extractEmojis, getAddedEmojis, getZWNJCursorOffset, insertZWNJBetweenDigitAndEmoji, replaceAndExtractEmojis} from '@libs/EmojiUtils'; +import {containsOnlyEmojis, extractEmojis, getAddedEmojis, getTextVSCursorOffset, insertTextVSBetweenDigitAndEmoji, replaceAndExtractEmojis} from '@libs/EmojiUtils'; import focusComposerWithDelay from '@libs/focusComposerWithDelay'; import type {ForwardedFSClassProps} from '@libs/Fullstory/types'; import getPlatform from '@libs/getPlatform'; @@ -429,8 +429,8 @@ function ComposerWithSuggestions({ const commentWithSpaceInserted = isEmojiInserted ? insertWhiteSpaceAtIndex(effectiveCommentValue, endIndex) : effectiveCommentValue; const {text: emojiConvertedText, emojis, cursorPosition} = replaceAndExtractEmojis(commentWithSpaceInserted, preferredSkinTone, preferredLocale); - const newComment = insertZWNJBetweenDigitAndEmoji(emojiConvertedText); - const zwnjOffset = getZWNJCursorOffset(emojiConvertedText, cursorPosition); + const newComment = insertTextVSBetweenDigitAndEmoji(emojiConvertedText); + const textVSOffset = getTextVSCursorOffset(emojiConvertedText, cursorPosition); if (emojis.length) { const newEmojis = getAddedEmojis(emojis, emojisPresentBefore.current); @@ -453,7 +453,7 @@ function ComposerWithSuggestions({ setValue(newCommentConverted); if (commentValue !== newComment) { - const adjustedCursorPosition = cursorPosition !== undefined && cursorPosition !== null ? cursorPosition + zwnjOffset : undefined; + const adjustedCursorPosition = cursorPosition !== undefined && cursorPosition !== null ? cursorPosition + textVSOffset : undefined; const position = Math.max((selection.end ?? 0) + (newComment.length - commentRef.current.length), adjustedCursorPosition ?? 0); if (commentWithSpaceInserted !== newComment && isIOSNative) { diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx index 35ec68e2b8dd..9870a471d77b 100644 --- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx @@ -37,7 +37,7 @@ import {deleteReportActionDraft, editReportComment, saveReportActionDraft} from import {isMobileChrome} from '@libs/Browser'; import {canSkipTriggerHotkeys, insertText} from '@libs/ComposerUtils'; import DomUtils from '@libs/DomUtils'; -import {extractEmojis, getZWNJCursorOffset, insertZWNJBetweenDigitAndEmoji, replaceAndExtractEmojis} from '@libs/EmojiUtils'; +import {extractEmojis, getTextVSCursorOffset, insertTextVSBetweenDigitAndEmoji, replaceAndExtractEmojis} from '@libs/EmojiUtils'; import focusComposerWithDelay from '@libs/focusComposerWithDelay'; import type {Selection} from '@libs/focusComposerWithDelay/types'; import focusEditAfterCancelDelete from '@libs/focusEditAfterCancelDelete'; @@ -246,15 +246,15 @@ function ReportActionItemMessageEdit({ (newDraftInput: string) => { raiseIsScrollLayoutTriggered(); const {text: emojiConvertedText, emojis, cursorPosition} = replaceAndExtractEmojis(newDraftInput, preferredSkinTone, preferredLocale); - const newDraft = insertZWNJBetweenDigitAndEmoji(emojiConvertedText); - const zwnjOffset = getZWNJCursorOffset(emojiConvertedText, cursorPosition); + const newDraft = insertTextVSBetweenDigitAndEmoji(emojiConvertedText); + const textVSOffset = getTextVSCursorOffset(emojiConvertedText, cursorPosition); emojisPresentBefore.current = emojis; setDraft(newDraft); if (newDraftInput !== newDraft) { - const adjustedCursorPosition = cursorPosition !== undefined && cursorPosition !== null ? cursorPosition + zwnjOffset : undefined; + const adjustedCursorPosition = cursorPosition !== undefined && cursorPosition !== null ? cursorPosition + textVSOffset : undefined; const position = Math.max((selection?.end ?? 0) + (newDraft.length - draftRef.current.length), adjustedCursorPosition ?? 0); setSelection({ start: position, diff --git a/tests/unit/EmojiTest.ts b/tests/unit/EmojiTest.ts index 23032d89d4e8..191a97b47608 100644 --- a/tests/unit/EmojiTest.ts +++ b/tests/unit/EmojiTest.ts @@ -460,9 +460,9 @@ describe('EmojiTest', () => { }); }); - describe('insertZWNJBetweenDigitAndEmoji', () => { - // ZWNJ character for comparison - const ZWNJ = '\u200C'; + describe('insertTextVSBetweenDigitAndEmoji', () => { + // FE0E (Variation Selector 15 - text presentation) for comparison + const FE0E = '\uFE0E'; // Mock isSafari to return true for these tests since the function only applies on Safari beforeEach(() => { @@ -473,38 +473,38 @@ describe('EmojiTest', () => { jest.restoreAllMocks(); }); - it('should insert ZWNJ between a single digit and emoji', () => { + it('should insert FE0E between a single digit and emoji', () => { // Given a digit immediately followed by an emoji const input = '1😄'; - // When we process it with insertZWNJBetweenDigitAndEmoji - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should be inserted between the digit and emoji - expect(result).toBe(`1${ZWNJ}😄`); + // When we process it with insertTextVSBetweenDigitAndEmoji + const result = EmojiUtils.insertTextVSBetweenDigitAndEmoji(input); + // Then FE0E should be inserted between the digit and emoji + expect(result).toBe(`1${FE0E}😄`); }); - it('should insert ZWNJ between multiple digits and emoji', () => { + it('should insert FE0E between multiple digits and emoji', () => { // Given multiple digits immediately followed by an emoji const input = '234😄'; - // When we process it with insertZWNJBetweenDigitAndEmoji - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should be inserted only between the last digit and emoji - expect(result).toBe(`234${ZWNJ}😄`); + // When we process it with insertTextVSBetweenDigitAndEmoji + const result = EmojiUtils.insertTextVSBetweenDigitAndEmoji(input); + // Then FE0E should be inserted only between the last digit and emoji + expect(result).toBe(`234${FE0E}😄`); }); it('should handle multiple digit-emoji pairs in the same string', () => { // Given a string with multiple digit-emoji pairs const input = '1😄 2🚀 3👍'; - // When we process it with insertZWNJBetweenDigitAndEmoji - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should be inserted for each pair - expect(result).toBe(`1${ZWNJ}😄 2${ZWNJ}🚀 3${ZWNJ}👍`); + // When we process it with insertTextVSBetweenDigitAndEmoji + const result = EmojiUtils.insertTextVSBetweenDigitAndEmoji(input); + // Then FE0E should be inserted for each pair + expect(result).toBe(`1${FE0E}😄 2${FE0E}🚀 3${FE0E}👍`); }); it('should not modify text with space between digit and emoji', () => { // Given a digit followed by a space and then an emoji const input = '1 😄'; - // When we process it with insertZWNJBetweenDigitAndEmoji - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); + // When we process it with insertTextVSBetweenDigitAndEmoji + const result = EmojiUtils.insertTextVSBetweenDigitAndEmoji(input); // Then the text should remain unchanged expect(result).toBe('1 😄'); }); @@ -512,8 +512,8 @@ describe('EmojiTest', () => { it('should not modify text with only digits', () => { // Given text with only digits const input = '12345'; - // When we process it with insertZWNJBetweenDigitAndEmoji - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); + // When we process it with insertTextVSBetweenDigitAndEmoji + const result = EmojiUtils.insertTextVSBetweenDigitAndEmoji(input); // Then the text should remain unchanged expect(result).toBe('12345'); }); @@ -521,8 +521,8 @@ describe('EmojiTest', () => { it('should not modify text with only emojis', () => { // Given text with only emojis const input = '😄🚀👍'; - // When we process it with insertZWNJBetweenDigitAndEmoji - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); + // When we process it with insertTextVSBetweenDigitAndEmoji + const result = EmojiUtils.insertTextVSBetweenDigitAndEmoji(input); // Then the text should remain unchanged expect(result).toBe('😄🚀👍'); }); @@ -530,8 +530,8 @@ describe('EmojiTest', () => { it('should not modify emoji followed by digit', () => { // Given an emoji followed by a digit const input = '😄1'; - // When we process it with insertZWNJBetweenDigitAndEmoji - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); + // When we process it with insertTextVSBetweenDigitAndEmoji + const result = EmojiUtils.insertTextVSBetweenDigitAndEmoji(input); // Then the text should remain unchanged expect(result).toBe('😄1'); }); @@ -539,8 +539,8 @@ describe('EmojiTest', () => { it('should handle empty string', () => { // Given an empty string const input = ''; - // When we process it with insertZWNJBetweenDigitAndEmoji - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); + // When we process it with insertTextVSBetweenDigitAndEmoji + const result = EmojiUtils.insertTextVSBetweenDigitAndEmoji(input); // Then the result should be an empty string expect(result).toBe(''); }); @@ -548,8 +548,8 @@ describe('EmojiTest', () => { it('should handle text without digits or emojis', () => { // Given regular text without digits or emojis const input = 'Hello World'; - // When we process it with insertZWNJBetweenDigitAndEmoji - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); + // When we process it with insertTextVSBetweenDigitAndEmoji + const result = EmojiUtils.insertTextVSBetweenDigitAndEmoji(input); // Then the text should remain unchanged expect(result).toBe('Hello World'); }); @@ -557,195 +557,111 @@ describe('EmojiTest', () => { it('should handle mixed content with digit-emoji pairs', () => { // Given mixed content with text, digits, and emojis const input = 'Hello 5😄 World'; - // When we process it with insertZWNJBetweenDigitAndEmoji - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should be inserted only between digit and emoji - expect(result).toBe(`Hello 5${ZWNJ}😄 World`); + // When we process it with insertTextVSBetweenDigitAndEmoji + const result = EmojiUtils.insertTextVSBetweenDigitAndEmoji(input); + // Then FE0E should be inserted only between digit and emoji + expect(result).toBe(`Hello 5${FE0E}😄 World`); }); it('should handle all digit types (0-9)', () => { // Given all digit types followed by emojis const inputs = ['0😄', '1😄', '2😄', '3😄', '4😄', '5😄', '6😄', '7😄', '8😄', '9😄']; - // When we process each with insertZWNJBetweenDigitAndEmoji - // Then ZWNJ should be inserted for each + // When we process each with insertTextVSBetweenDigitAndEmoji + // Then FE0E should be inserted for each for (const input of inputs) { - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - expect(result).toBe(`${input[0]}${ZWNJ}${input.slice(1)}`); + const result = EmojiUtils.insertTextVSBetweenDigitAndEmoji(input); + expect(result).toBe(`${input[0]}${FE0E}${input.slice(1)}`); } }); it('should handle various emoji types from different Unicode ranges', () => { // Given digits followed by emojis from different Unicode ranges // Miscellaneous Symbols (U+2600-U+27BF) - expect(EmojiUtils.insertZWNJBetweenDigitAndEmoji('1☀')).toBe(`1${ZWNJ}☀`); + expect(EmojiUtils.insertTextVSBetweenDigitAndEmoji('1☀')).toBe(`1${FE0E}☀`); // Miscellaneous Symbols and Pictographs (U+1F300-U+1F5FF) - expect(EmojiUtils.insertZWNJBetweenDigitAndEmoji('1🌟')).toBe(`1${ZWNJ}🌟`); + expect(EmojiUtils.insertTextVSBetweenDigitAndEmoji('1🌟')).toBe(`1${FE0E}🌟`); // Emoticons (U+1F600-U+1F64F) - expect(EmojiUtils.insertZWNJBetweenDigitAndEmoji('1😀')).toBe(`1${ZWNJ}😀`); + expect(EmojiUtils.insertTextVSBetweenDigitAndEmoji('1😀')).toBe(`1${FE0E}😀`); // Transport and Map Symbols (U+1F680-U+1F6FF) - expect(EmojiUtils.insertZWNJBetweenDigitAndEmoji('1🚀')).toBe(`1${ZWNJ}🚀`); + expect(EmojiUtils.insertTextVSBetweenDigitAndEmoji('1🚀')).toBe(`1${FE0E}🚀`); }); it('should handle consecutive digit-emoji pairs without spaces', () => { // Given consecutive digit-emoji pairs const input = '1😄2🚀3👍'; - // When we process it with insertZWNJBetweenDigitAndEmoji - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should be inserted for each pair - expect(result).toBe(`1${ZWNJ}😄2${ZWNJ}🚀3${ZWNJ}👍`); + // When we process it with insertTextVSBetweenDigitAndEmoji + const result = EmojiUtils.insertTextVSBetweenDigitAndEmoji(input); + // Then FE0E should be inserted for each pair + expect(result).toBe(`1${FE0E}😄2${FE0E}🚀3${FE0E}👍`); }); it('should simulate the Safari keycap bug scenario - typing "234:smile:"', () => { // Given the scenario where a user types "234" then adds :smile: emoji // After emoji shortcode conversion, we get "234😄" const afterEmojiConversion = '234😄'; - // When we apply the ZWNJ fix - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(afterEmojiConversion); - // Then ZWNJ should be inserted to prevent Safari's keycap sequence detection - expect(result).toBe(`234${ZWNJ}😄`); - // Verify the ZWNJ is actually in the string - expect(result.includes(ZWNJ)).toBe(true); - // Verify the result is different from input (ZWNJ was added) + // When we apply the FE0E fix + const result = EmojiUtils.insertTextVSBetweenDigitAndEmoji(afterEmojiConversion); + // Then FE0E should be inserted to prevent Safari's keycap sequence detection + expect(result).toBe(`234${FE0E}😄`); + // Verify the FE0E is actually in the string + expect(result.includes(FE0E)).toBe(true); + // Verify the result is different from input (FE0E was added) expect(result.length).toBe(afterEmojiConversion.length + 1); }); - it('should insert ZWNJ between hash symbol (#) and emoji', () => { - // Given a hash symbol immediately followed by an emoji - const input = '#😄'; - // When we process it with insertZWNJBetweenDigitAndEmoji - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should be inserted between the hash and emoji - expect(result).toBe(`#${ZWNJ}😄`); - }); - - it('should insert ZWNJ between asterisk symbol (*) and emoji', () => { - // Given an asterisk symbol immediately followed by an emoji - const input = '*😄'; - // When we process it with insertZWNJBetweenDigitAndEmoji - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should be inserted between the asterisk and emoji - expect(result).toBe(`*${ZWNJ}😄`); - }); - - it('should handle mixed digits and symbols (#, *) followed by emojis', () => { - // Given a string with digits, hash, and asterisk followed by emojis - const input = '1😄 #🚀 *👍'; - // When we process it with insertZWNJBetweenDigitAndEmoji - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should be inserted only between digit/symbol and emoji (not emoji-to-symbol) - expect(result).toBe(`1${ZWNJ}😄 #${ZWNJ}🚀 *${ZWNJ}👍`); - }); - - it('should handle consecutive symbol-emoji pairs (# and *)', () => { - // Given consecutive symbol-emoji pairs - const input = '#😄*🚀'; - // When we process it with insertZWNJBetweenDigitAndEmoji - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should be inserted only between symbol and emoji (not emoji-to-symbol) - expect(result).toBe(`#${ZWNJ}😄*${ZWNJ}🚀`); - }); - - it('should not modify text with space between symbol (# or *) and emoji', () => { - // Given a symbol followed by a space and then an emoji - expect(EmojiUtils.insertZWNJBetweenDigitAndEmoji('# 😄')).toBe('# 😄'); - expect(EmojiUtils.insertZWNJBetweenDigitAndEmoji('* 😄')).toBe('* 😄'); - }); - it('should return input unchanged on non-Safari browsers', () => { // Given we're not on Safari jest.spyOn(Browser, 'isSafari').mockReturnValue(false); // When we process a digit + emoji string const input = '234😄'; - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then the text should remain unchanged (no ZWNJ inserted) + const result = EmojiUtils.insertTextVSBetweenDigitAndEmoji(input); + // Then the text should remain unchanged (no FE0E inserted) expect(result).toBe('234😄'); - expect(result.includes(ZWNJ)).toBe(false); + expect(result.includes(FE0E)).toBe(false); }); - it('should not insert ZWNJ between emoji and asterisk (to preserve cursor navigation)', () => { - // Given an emoji followed by asterisk - // We don't insert ZWNJ here to avoid cursor navigation issues - const input = '#😄*'; - // When we process it - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should only be inserted between # and emoji (not emoji-to-symbol) - expect(result).toBe(`#${ZWNJ}😄*`); + it('should insert FE0E between hash symbol (#) and emoji', () => { + // Given a hash symbol immediately followed by an emoji + const input = '#😄'; + // When we process it with insertTextVSBetweenDigitAndEmoji + const result = EmojiUtils.insertTextVSBetweenDigitAndEmoji(input); + // Then FE0E should be inserted between the hash and emoji + expect(result).toBe(`#${FE0E}😄`); }); - it('should handle emoji followed by asterisk then emoji', () => { - // Given emoji, asterisk, then emoji (like "#😄*😀") - const input = '#😄*😀'; - // When we process it - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then ZWNJ should be inserted between # and first emoji, and between * and second emoji (not emoji-to-symbol) - expect(result).toBe(`#${ZWNJ}😄*${ZWNJ}😀`); + it('should insert FE0E between asterisk symbol (*) and emoji', () => { + // Given an asterisk symbol immediately followed by an emoji + const input = '*😄'; + // When we process it with insertTextVSBetweenDigitAndEmoji + const result = EmojiUtils.insertTextVSBetweenDigitAndEmoji(input); + // Then FE0E should be inserted between the asterisk and emoji + expect(result).toBe(`*${FE0E}😄`); }); - it('should not insert ZWNJ between emoji and hash symbol (to preserve cursor navigation)', () => { - // Given an emoji followed by hash symbol - const input = '😄#'; - // When we process it - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - // Then no ZWNJ should be inserted (emoji-to-symbol case is not modified) - expect(result).toBe('😄#'); + it('should handle mixed digits and symbols (#, *) followed by emojis', () => { + // Given a string with digits, hash, and asterisk followed by emojis + const input = '1😄 #🚀 *👍'; + // When we process it with insertTextVSBetweenDigitAndEmoji + const result = EmojiUtils.insertTextVSBetweenDigitAndEmoji(input); + // Then FE0E should be inserted between each symbol/digit and emoji + expect(result).toBe(`1${FE0E}😄 #${FE0E}🚀 *${FE0E}👍`); }); it('should fix corrupted keycap sequence followed by emoji', () => { // Given Safari has created "*️⃣😄" (corrupted keycap + emoji) - // This happens when Safari corrupts "*😄" to "*️⃣😄" before React processes it - const corruptedKeycapWithEmoji = '*\uFE0F\u20E3😄'; // *️⃣😄 - - // When we process it - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(corruptedKeycapWithEmoji); - - // Then it should be converted to "*\u200C😄" (preserving the emoji) - expect(result).toBe(`*${ZWNJ}😄`); - }); - - it('should handle space between symbol and emoji correctly', () => { - // Given text with space between symbol and emoji (like "#😃 *😄") - const input = '#😃 *😄'; - + const corruptedKeycapWithEmoji = '*\uFE0F\u20E3😄'; // When we process it - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - - // Then ZWNJ should only be inserted between symbol and emoji (not emoji-to-symbol) - expect(result).toBe(`#${ZWNJ}😃 *${ZWNJ}😄`); - }); - - it('should fix corrupted keycap in text with spaces', () => { - // Given text like "#😃 *️⃣😄" where Safari corrupted "*😄" to "*️⃣😄" - const input = '#😃 *\uFE0F\u20E3😄'; - - // When we process it - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - - // Then corrupted keycap should be fixed to "*\u200C😄" (keycap removed, ZWNJ added) - expect(result).toBe(`#${ZWNJ}😃 *${ZWNJ}😄`); - }); - - it('should not modify emoji followed by spaces and symbol', () => { - // Given emoji followed by one or more spaces then symbol (like "#😄 *" or "#😄 *") - const inputOneSpace = '#😄 *'; - const inputTwoSpaces = '#😄 *'; - - // When we process it - const resultOneSpace = EmojiUtils.insertZWNJBetweenDigitAndEmoji(inputOneSpace); - const resultTwoSpaces = EmojiUtils.insertZWNJBetweenDigitAndEmoji(inputTwoSpaces); - - // Then no ZWNJ should be inserted between emoji and symbol (only between # and emoji) - expect(resultOneSpace).toBe(`#${ZWNJ}😄 *`); - expect(resultTwoSpaces).toBe(`#${ZWNJ}😄 *`); + const result = EmojiUtils.insertTextVSBetweenDigitAndEmoji(corruptedKeycapWithEmoji); + // Then it should be converted to "*\uFE0E😄" + expect(result).toBe(`*${FE0E}😄`); }); it('should preserve legitimate standalone keycap emojis', () => { // Given a legitimate standalone keycap emoji (like "*️⃣") const input = '*\uFE0F\u20E3'; - // When we process it - const result = EmojiUtils.insertZWNJBetweenDigitAndEmoji(input); - - // Then the keycap should be preserved (not removed) since it's not followed by another emoji + const result = EmojiUtils.insertTextVSBetweenDigitAndEmoji(input); + // Then the keycap should be preserved (not followed by another emoji) expect(result).toBe('*\uFE0F\u20E3'); }); });