From 4c467c511846dd57b43803103bd7bb373f2713b4 Mon Sep 17 00:00:00 2001 From: Uzaifm127 Date: Fri, 4 Sep 2026 22:51:51 +0530 Subject: [PATCH 1/4] chore: bump react-native-live-markdown to 0.1.337 --- package-lock.json | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6790620e41eb..0f71861c4490 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "@expensify/nitro-utils": "file:./modules/ExpensifyNitroUtils", "@expensify/react-native-background-task": "file:./modules/background-task", "@expensify/react-native-hybrid-app": "file:./modules/hybrid-app", - "@expensify/react-native-live-markdown": "0.1.336", + "@expensify/react-native-live-markdown": "0.1.337", "@expensify/react-native-wallet": "0.1.22", "@expo/metro-config": "57.0.7", "@expo/metro-runtime": "57.0.7", @@ -5533,9 +5533,9 @@ "link": true }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.336", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.336.tgz", - "integrity": "sha512-rrbcQmiTNdv7iySVbQWoY7wMGpIf9e+Wx7LJkx23AGkGwAkq7Mfptsly/4a6GaunIQ3Qh5+D84cLxlI42rLlWQ==", + "version": "0.1.337", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.337.tgz", + "integrity": "sha512-+I3jCqTLIq0+dRgAUXXZZB+0HYXcuQ7uECm6VsFxiiHuPaKZ647RnLSDfJ0+1ZBBGHQJjMtkBYNcv/xs/I52tw==", "license": "MIT", "workspaces": [ "./example", @@ -5545,7 +5545,7 @@ "node": ">= 18.0.0" }, "peerDependencies": { - "expensify-common": ">=2.0.189", + "expensify-common": ">=2.0.201", "react": "*", "react-native": "*", "react-native-worklets": ">=0.7.0" diff --git a/package.json b/package.json index 1b1eba9e36f8..951bf1284d66 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "@expensify/nitro-utils": "file:./modules/ExpensifyNitroUtils", "@expensify/react-native-background-task": "file:./modules/background-task", "@expensify/react-native-hybrid-app": "file:./modules/hybrid-app", - "@expensify/react-native-live-markdown": "0.1.336", + "@expensify/react-native-live-markdown": "0.1.337", "@expensify/react-native-wallet": "0.1.22", "@expo/metro-config": "57.0.7", "@expo/metro-runtime": "57.0.7", From 9f6e86afeff27d502fefddd740db4ef00c017af3 Mon Sep 17 00:00:00 2001 From: Uzaifm127 Date: Fri, 4 Sep 2026 23:56:43 +0530 Subject: [PATCH 2/4] fix: pass max markup length to parseExpensiMark --- src/libs/FormatSelectionUtils.ts | 4 +++- src/libs/ParsingUtils.ts | 2 +- tests/unit/FormatSelectionUtilsTest.ts | 8 ++++++++ tests/unit/libs/ParsingUtilsTest.ts | 16 +++++++++++++++- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/libs/FormatSelectionUtils.ts b/src/libs/FormatSelectionUtils.ts index eaae9f14633e..d8b417dc33e1 100644 --- a/src/libs/FormatSelectionUtils.ts +++ b/src/libs/FormatSelectionUtils.ts @@ -1,3 +1,5 @@ +import CONST from '@src/CONST'; + import type {MarkdownType} from '@expensify/react-native-live-markdown'; import {parseExpensiMark} from '@expensify/react-native-live-markdown'; @@ -23,7 +25,7 @@ function applyFormatting(text: string, selectionStart: number, selectionEnd: num } function findMatchingFormat(text: string, selectionStart: number, selectionEnd: number, formatRule: FormatRule): Match | null { - const markdownRanges = parseExpensiMark(text); + const markdownRanges = parseExpensiMark(text, CONST.MAX_MARKUP_LENGTH); for (const range of markdownRanges) { if (range?.type === formatRule.markdownType && range.start != null && range.length != null) { const rangeEnd = range.start + range.length; diff --git a/src/libs/ParsingUtils.ts b/src/libs/ParsingUtils.ts index bfdbf409ff77..2d1b2010c1d9 100644 --- a/src/libs/ParsingUtils.ts +++ b/src/libs/ParsingUtils.ts @@ -61,7 +61,7 @@ function decorateRangesWithShortMentions(ranges: MarkdownRange[], text: string, function parseExpensiMarkWithShortMentions(text: string, availableMentions: string[], currentUserMentions?: string[]) { 'worklet'; - const parsedRanges = parseExpensiMark(text); + const parsedRanges = parseExpensiMark(text, CONST.MAX_MARKUP_LENGTH); return decorateRangesWithShortMentions(parsedRanges, text, availableMentions, currentUserMentions); } diff --git a/tests/unit/FormatSelectionUtilsTest.ts b/tests/unit/FormatSelectionUtilsTest.ts index a5bd1d80abd2..f64ff56b7672 100644 --- a/tests/unit/FormatSelectionUtilsTest.ts +++ b/tests/unit/FormatSelectionUtilsTest.ts @@ -1,5 +1,7 @@ import toggleSelectionFormat from '@libs/FormatSelectionUtils'; +import CONST from '@src/CONST'; + jest.unmock('@expensify/react-native-live-markdown'); describe('FormatSelectionUtils', () => { @@ -79,6 +81,12 @@ describe('FormatSelectionUtils', () => { expect(toggleSelectionFormat('_aaa_ _*bbb*_ _ccc_', 7, 12, 'formatItalic')).toEqual({updatedText: '_aaa_ *bbb* _ccc_', cursorOffset: -1}); }); + it('remove formatting from a long Markdown range', () => { + const text = `*${'a'.repeat(CONST.MAX_MARKUP_LENGTH - 2)}*`; + + expect(toggleSelectionFormat(text, 1, text.length - 1, 'formatBold')).toEqual({updatedText: 'a'.repeat(CONST.MAX_MARKUP_LENGTH - 2), cursorOffset: -1}); + }); + it('do nothing for unsupported command', () => { expect(toggleSelectionFormat('aaa', 0, 3, 'formatUnderline')).toEqual({updatedText: 'aaa', cursorOffset: 0}); expect(toggleSelectionFormat('_aaa_', 1, 4, 'formatUnderline')).toEqual({updatedText: '_aaa_', cursorOffset: 0}); diff --git a/tests/unit/libs/ParsingUtilsTest.ts b/tests/unit/libs/ParsingUtilsTest.ts index 22d2102a5f57..84036301fd72 100644 --- a/tests/unit/libs/ParsingUtilsTest.ts +++ b/tests/unit/libs/ParsingUtilsTest.ts @@ -1,9 +1,13 @@ -import {decorateRangesWithShortMentions, getParsedMessageWithShortMentions} from '@libs/ParsingUtils'; +import {decorateRangesWithShortMentions, getParsedMessageWithShortMentions, parseExpensiMarkWithShortMentions} from '@libs/ParsingUtils'; + +import CONST from '@src/CONST'; import type {MarkdownRange} from '@expensify/react-native-live-markdown'; const TEST_COMPANY_DOMAIN = 'myCompany.com'; +jest.unmock('@expensify/react-native-live-markdown'); + describe('decorateRangesWithShortMentions', () => { test('returns empty list for empty text', () => { const result = decorateRangesWithShortMentions([], '', [], []); @@ -223,3 +227,13 @@ describe('getParsedMessageWithShortMentions', () => { expect(result).toEqual(`this is @john.doe@myCompany.com's mention`); }); }); + +describe('parseExpensiMarkWithShortMentions', () => { + test('parses Markdown up to the App markup limit', () => { + const text = `*${'a'.repeat(CONST.MAX_MARKUP_LENGTH - 2)}*`; + + const result = parseExpensiMarkWithShortMentions(text, [], []); + + expect(result).toContainEqual({type: 'bold', start: 1, length: CONST.MAX_MARKUP_LENGTH - 2}); + }); +}); From 50c73edadc4a1a5aab52196b34109b381b9ee3b7 Mon Sep 17 00:00:00 2001 From: Uzaifm127 Date: Sun, 6 Sep 2026 13:23:54 +0530 Subject: [PATCH 3/4] fix: preserve emoji parsing in long messages Pass CONST.MAX_MARKUP_LENGTH to all emoji-related parseExpensiMark calls --- src/libs/EmojiUtils.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/EmojiUtils.tsx b/src/libs/EmojiUtils.tsx index bbae6bf0729c..3a4f4acdcb56 100644 --- a/src/libs/EmojiUtils.tsx +++ b/src/libs/EmojiUtils.tsx @@ -43,7 +43,7 @@ const findEmojiByCode = (code: string): Emoji => Emojis.emojiCodeTableWithSkinTo const CODE_RANGE_TYPES = new Set(['code', 'pre']); function getCodeRanges(text: string): MarkdownRange[] { - return parseExpensiMark(text).filter((range) => CODE_RANGE_TYPES.has(range.type)); + return parseExpensiMark(text, CONST.MAX_MARKUP_LENGTH).filter((range) => CODE_RANGE_TYPES.has(range.type)); } function isPositionInsideCodeRanges(ranges: MarkdownRange[], position: number): boolean { @@ -51,7 +51,7 @@ function isPositionInsideCodeRanges(ranges: MarkdownRange[], position: number): } function isPositionInsideCodeBlock(text: string, position: number): boolean { - return isPositionInsideCodeRanges(parseExpensiMark(text), position); + return isPositionInsideCodeRanges(parseExpensiMark(text, CONST.MAX_MARKUP_LENGTH), position); } /** @@ -462,7 +462,7 @@ function replaceEmojis(text: string, preferredSkinTone: OnyxEntry = []; const shortcodeSearchPositions: Record = {}; const englishTrie = normalizedLocale !== CONST.LOCALES.DEFAULT ? getEmojiTrie(CONST.LOCALES.DEFAULT) : null; From 1f7aa120782eb263cd02e085bbd4a63714690db6 Mon Sep 17 00:00:00 2001 From: Uzaifm127 Date: Sun, 6 Sep 2026 13:34:56 +0530 Subject: [PATCH 4/4] test: cover emoji parsing in long messages Add regression coverage for emoji reversion, shortcode replacement, and suggestion detection at the 10,000-character markup limit. --- tests/unit/EmojiTest.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/unit/EmojiTest.ts b/tests/unit/EmojiTest.ts index 8272ceb9e5e4..e6a7b4f5a766 100644 --- a/tests/unit/EmojiTest.ts +++ b/tests/unit/EmojiTest.ts @@ -5,6 +5,7 @@ import * as Browser from '@libs/Browser'; import emojiTrieForLocale, {buildEmojisTrie} from '@libs/EmojiTrie'; import * as EmojiUtils from '@libs/EmojiUtils'; +import CONST from '@src/CONST'; import type FrequentlyUsedEmoji from '@src/types/onyx/FrequentlyUsedEmoji'; import type {ReportActionReaction} from '@src/types/onyx/ReportActionReactions'; @@ -176,6 +177,12 @@ describe('EmojiTest', () => { expect(EmojiUtils.replaceEmojis(text).text).toBe('`:smile:`'); }); + it('should revert emoji unicode inside a code block at the maximum markup length', () => { + const code = '`šŸ˜„`'; + const prefix = 'a'.repeat(CONST.MAX_MARKUP_LENGTH - code.length); + expect(EmojiUtils.replaceEmojis(`${prefix}${code}`).text).toBe(`${prefix}\`:smile:\``); + }); + it('should revert multiple emojis inside code block', () => { const text = '`šŸ˜„šŸ‘‹`'; expect(EmojiUtils.replaceEmojis(text).text).toBe('`:smile::wave:`'); @@ -191,6 +198,12 @@ describe('EmojiTest', () => { expect(EmojiUtils.replaceEmojis(text).text).toBe('šŸ‘‹ hello `:smile:` world'); }); + it('should replace a shortcode outside code but preserve one inside code at the maximum markup length', () => { + const markdown = '\n:smile: and `:wave:`'; + const prefix = 'a'.repeat(CONST.MAX_MARKUP_LENGTH - markdown.length); + expect(EmojiUtils.replaceEmojis(`${prefix}${markdown}`).text).toBe(`${prefix}\nšŸ˜„ and \`:wave:\``); + }); + it('should handle same shortcode both inside and outside code block', () => { // Regression test: indexOf was returning the first occurrence for both, // causing the shortcode outside to not be converted @@ -284,6 +297,13 @@ describe('EmojiTest', () => { expect(EmojiUtils.isPositionInsideCodeBlock(text, 1)).toBe(true); }); + it('should return true for a position inside inline code at the maximum markup length', () => { + const code = '`:smi`'; + const prefix = 'a'.repeat(CONST.MAX_MARKUP_LENGTH - code.length); + const text = `${prefix}${code}`; + expect(EmojiUtils.isPositionInsideCodeBlock(text, prefix.length + 1)).toBe(true); + }); + it('should return false for position outside code block', () => { const text = 'hello `:joy:`'; // Position 0 is 'h' which is outside