diff --git a/cspell.json b/cspell.json
index 1c61490d481e..660c913389f5 100644
--- a/cspell.json
+++ b/cspell.json
@@ -8,6 +8,7 @@
"words": [
"--longpress",
"Accelo",
+ "accountid",
"achreimburse",
"actool",
"adbd",
@@ -43,6 +44,7 @@
"approvalstatus",
"appversion",
"archivado",
+ "Areport",
"ARGB",
"armeabi",
"armv7",
@@ -57,7 +59,6 @@
"asar",
"ASPAC",
"assetlinks",
- "accountid",
"attributes.accountid",
"attributes.reportid",
"authorised",
@@ -104,17 +105,17 @@
"Buildscript",
"Bushwick",
"BYOC",
+ "cacerts",
"capitalone",
"CAROOT",
"Carta",
- "cacerts",
- "changeit",
"ccache",
"ccupload",
"cdfbmo",
"Certinia",
"Certinia's",
"CFPB",
+ "changeit",
"chargeback",
"Charleson",
"Checkmark",
@@ -140,6 +141,7 @@
"contenteditable",
"copiloted",
"copiloting",
+ "copyable",
"Corpay",
"Countertop",
"CPPFLAGS",
@@ -152,13 +154,12 @@
"customfield",
"customise",
"dateexported",
+ "deapex",
+ "deapexer",
"debitamount",
"deburr",
"deburred",
- "REJECTEDTRANSACTION",
"Deel",
- "deapex",
- "deapexer",
"deeplink",
"deeplinked",
"deeplinking",
@@ -183,7 +184,9 @@
"Drycleaning",
"DSYM",
"dsyms",
+ "Dtype",
"durationMillis",
+ "DYNAMICEXTERNAL",
"e2edelta",
"ecash",
"ecconnrefused",
@@ -223,12 +226,12 @@
"Expensable",
"expensescount",
"Expensi",
- "Expensidev",
"Expensicon",
"Expensicons",
"expensicorp",
- "Expensifier",
+ "Expensidev",
"EXPENSIDEV",
+ "Expensifier",
"EXPENSIFYAPI",
"expensifyhelp",
"expensifylite",
@@ -348,8 +351,8 @@
"keyvaluepairs",
"KHTML",
"killall",
- "kilometres",
"kilometre",
+ "kilometres",
"Kort",
"Kowalski",
"Krasoń",
@@ -407,6 +410,9 @@
"Mobasher",
"mobiexpensifyg",
"mobileprovision",
+ "moveElemsAttrsToGroup",
+ "moveGroupAttrsToElems",
+ "mple",
"mswin",
"msword",
"mtrl",
@@ -452,9 +458,9 @@
"Nonstore",
"Nonupholstered",
"noopener",
+ "noprompt",
"noreferer",
"noreferrer",
- "noprompt",
"nosymbol",
"Noto",
"NSQS",
@@ -477,6 +483,7 @@
"OLDDOT",
"onclosetag",
"Oncorp",
+ "oneline",
"oneteam",
"oneui",
"onfido",
@@ -577,8 +584,10 @@
"REIMBURSER",
"reimbursible",
"Reimbursments",
+ "REJECTEDTRANSACTION",
"remotedesktop",
"remotesync",
+ "removeHiddenElems",
"REPORTPREVIEW",
"requestee",
"Resawing",
@@ -587,11 +596,12 @@
"retryable",
"Reupholstery",
"rideshare",
- "rock",
+ "RNCORE",
"RNFS",
"rnmapbox",
"RNTL",
"RNVP",
+ "rock",
"Roni",
"Rosiclair",
"rpartition",
@@ -611,18 +621,19 @@
"Schengen",
"Schiffli",
"SCIM",
- "sdkmanager",
"scriptname",
+ "sdkmanager",
"seamless",
"Segoe",
"seguiemj",
+ "Selec",
"Sepa",
"serveo",
+ "setuptools",
"Sharees",
"Sharons",
"shellcheck",
"shellenv",
- "Skydo",
"shipit",
"shouldshowellipsis",
"signingkey",
@@ -630,21 +641,23 @@
"Signup",
"simctl",
"skip_codesigning",
+ "Skydo",
"Slurper",
"SMARTREPORT",
"Smartscan",
"soloader",
"SONIFICATION",
"Speedscope",
+ "Splittable",
"Spotnana",
"spreadsheetml",
"srgb",
"SSAE",
+ "stackoverflow",
"startdate",
"stdev",
"stdlib",
"storepass",
- "stackoverflow",
"STORYLANE",
"strikethrough",
"Strikethrough",
@@ -752,6 +765,7 @@
"webrtc",
"welldone",
"Woohoo",
+ "Wooo",
"Wordmark",
"wordprocessingml",
"worklet",
@@ -791,21 +805,7 @@
"zoneinfo",
"zxcv",
"zxldvw",
- "inputmethod",
- "copyable",
- "مثال",
- "moveElemsAttrsToGroup",
- "removeHiddenElems",
- "moveGroupAttrsToElems",
- "Dtype",
- "Areport",
- "mple",
- "Selec",
- "setuptools",
- "DYNAMICEXTERNAL",
- "RNCORE",
- "Wooo",
- "Splittable"
+ "مثال"
],
"ignorePaths": [
"src/languages/de.ts",
diff --git a/src/components/EmojiWithTooltip/index.ios.tsx b/src/components/EmojiWithTooltip/index.ios.tsx
index 3b27d14123b2..dc58e010157c 100644
--- a/src/components/EmojiWithTooltip/index.ios.tsx
+++ b/src/components/EmojiWithTooltip/index.ios.tsx
@@ -3,9 +3,12 @@ import Text from '@components/Text';
import useThemeStyles from '@hooks/useThemeStyles';
import type EmojiWithTooltipProps from './types';
-function EmojiWithTooltip({emojiCode, style = {}, isMedium = false}: EmojiWithTooltipProps) {
+function EmojiWithTooltip({emojiCode, style = {}, isMedium = false, isOnSeparateLine = false}: EmojiWithTooltipProps) {
const styles = useThemeStyles();
const isCustomEmoji = emojiCode === '\uE100';
+ if (isOnSeparateLine) {
+ return {emojiCode};
+ }
return isMedium ? (
diff --git a/src/components/EmojiWithTooltip/types.ts b/src/components/EmojiWithTooltip/types.ts
index 1e7993198e98..31fe4d1ada4c 100644
--- a/src/components/EmojiWithTooltip/types.ts
+++ b/src/components/EmojiWithTooltip/types.ts
@@ -4,6 +4,7 @@ type EmojiWithTooltipProps = {
emojiCode: string;
style?: StyleProp;
isMedium?: boolean;
+ isOnSeparateLine?: boolean;
};
export default EmojiWithTooltipProps;
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx
index 80c67f4ae11b..13c2d0cafb7d 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx
@@ -1,4 +1,4 @@
-import React, {useMemo} from 'react';
+import React from 'react';
import type {TextStyle} from 'react-native';
import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-html';
import EmojiWithTooltip from '@components/EmojiWithTooltip';
@@ -6,19 +6,19 @@ import useThemeStyles from '@hooks/useThemeStyles';
function EmojiRenderer({tnode, style: styleProp}: CustomRendererProps) {
const styles = useThemeStyles();
- const style = useMemo(() => {
- if ('islarge' in tnode.attributes) {
- return [styleProp as TextStyle, styles.onlyEmojisText];
- }
- if ('ismedium' in tnode.attributes) {
- return [styleProp as TextStyle, styles.emojisWithTextFontSize, styles.verticalAlignTopText];
- }
+ let style;
+ if ('islarge' in tnode.attributes) {
+ style = [styleProp as TextStyle, styles.onlyEmojisText];
+ } else if ('ismedium' in tnode.attributes) {
+ style = [styleProp as TextStyle, styles.emojisWithTextFontSize, styles.verticalAlignTopText];
+ } else {
+ style = null;
+ }
- return null;
- }, [tnode.attributes, styles, styleProp]);
return (
/gi, '').trim();
+ return /^.*<\/emoji>$/.test(trimmed);
+}
+
export type {HeaderIndices, EmojiPickerList, EmojiPickerListItem};
export {
@@ -725,4 +730,5 @@ export {
processFrequentlyUsedEmojis,
insertZWNJBetweenDigitAndEmoji,
getZWNJCursorOffset,
+ isEmojiOnSeparateLine,
};
diff --git a/src/pages/home/report/comment/TextCommentFragment.tsx b/src/pages/home/report/comment/TextCommentFragment.tsx
index 7ad1069b78a8..37d672977772 100644
--- a/src/pages/home/report/comment/TextCommentFragment.tsx
+++ b/src/pages/home/report/comment/TextCommentFragment.tsx
@@ -1,6 +1,6 @@
import {Str} from 'expensify-common';
import isEmpty from 'lodash/isEmpty';
-import React, {memo, useEffect, useMemo} from 'react';
+import React, {useEffect} from 'react';
import type {StyleProp, TextStyle} from 'react-native';
import Text from '@components/Text';
import ZeroWidthView from '@components/ZeroWidthView';
@@ -11,7 +11,7 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import convertToLTR from '@libs/convertToLTR';
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
-import {containsOnlyCustomEmoji as containsOnlyCustomEmojiUtil, containsOnlyEmojis as containsOnlyEmojisUtil, splitTextWithEmojis} from '@libs/EmojiUtils';
+import {containsOnlyCustomEmoji as containsOnlyCustomEmojiUtil, containsOnlyEmojis as containsOnlyEmojisUtil, isEmojiOnSeparateLine, splitTextWithEmojis} from '@libs/EmojiUtils';
import Parser from '@libs/Parser';
import Performance from '@libs/Performance';
import {getHtmlWithAttachmentID, getTextFromHtml} from '@libs/ReportActionsUtils';
@@ -62,7 +62,7 @@ function TextCommentFragment({fragment, styleAsDeleted, reportActionID, styleAsM
const message = isEmpty(iouMessage) ? text : iouMessage;
- const processedTextArray = useMemo(() => splitTextWithEmojis(message), [message]);
+ const processedTextArray = splitTextWithEmojis(message);
useEffect(() => {
Performance.markEnd(CONST.TIMING.SEND_MESSAGE, {message: text});
@@ -74,7 +74,7 @@ function TextCommentFragment({fragment, styleAsDeleted, reportActionID, styleAsM
// on native, we render it as text, not as html
// on other device, only render it as text if the only difference is
tag
const containsOnlyEmojis = containsOnlyEmojisUtil(text ?? '');
- const containsOnlyCustomEmoji = useMemo(() => containsOnlyCustomEmojiUtil(text), [text]);
+ const containsOnlyCustomEmoji = containsOnlyCustomEmojiUtil(text);
const containsEmojis = CONST.REGEX.ALL_EMOJIS.test(text ?? '');
if (!shouldRenderAsText(html, text ?? '') && !(containsOnlyEmojis && styleAsDeleted)) {
const editedTag = fragment?.isEdited ? `` : '';
@@ -90,7 +90,15 @@ function TextCommentFragment({fragment, styleAsDeleted, reportActionID, styleAsM
if (!htmlContent.includes('')) {
htmlContent = Parser.replace(htmlContent, {filterRules: ['emoji'], shouldEscapeText: false});
}
- htmlContent = Str.replaceAll(htmlContent, '', '');
+ const lines = htmlContent.split(/
/i);
+ const processedLines = lines.map((line) => {
+ if (isEmojiOnSeparateLine(line)) {
+ return line.replace('', '');
+ }
+ return line.replace('', '');
+ });
+
+ htmlContent = processedLines.join('
');
}
let htmlWithTag = editedTag ? `${htmlContent}${editedTag}` : htmlContent;
@@ -158,4 +166,4 @@ function TextCommentFragment({fragment, styleAsDeleted, reportActionID, styleAsM
);
}
-export default memo(TextCommentFragment);
+export default TextCommentFragment;
diff --git a/tests/unit/libs/EmojiUtilsTest.ts b/tests/unit/libs/EmojiUtilsTest.ts
index f9e27ef92043..e475991a54f0 100644
--- a/tests/unit/libs/EmojiUtilsTest.ts
+++ b/tests/unit/libs/EmojiUtilsTest.ts
@@ -1,4 +1,4 @@
-import {processFrequentlyUsedEmojis} from '@libs/EmojiUtils';
+import {isEmojiOnSeparateLine, processFrequentlyUsedEmojis} from '@libs/EmojiUtils';
import type {FrequentlyUsedEmoji} from '@src/types/onyx';
// Mock the Emojis module
@@ -302,3 +302,69 @@ describe('processFrequentlyUsedEmojis', () => {
expect(result.at(1)?.lastUpdatedAt).toBe(1500);
});
});
+
+describe('isEmojiOnSeparateLine', () => {
+ it('should return true for a simple single emoji line', () => {
+ expect(isEmojiOnSeparateLine('😀')).toBe(true);
+ });
+
+ it('should return true for emoji line with whitespace', () => {
+ expect(isEmojiOnSeparateLine(' 😀 ')).toBe(true);
+ });
+
+ it('should return true for emoji line with
tag before', () => {
+ expect(isEmojiOnSeparateLine('
😀')).toBe(true);
+ });
+
+ it('should return true for emoji line with
tag after', () => {
+ expect(isEmojiOnSeparateLine('😀
')).toBe(true);
+ });
+
+ it('should return true for emoji line with
tag', () => {
+ expect(isEmojiOnSeparateLine('
😀
')).toBe(true);
+ });
+
+ it('should return true for emoji line with multiple
tags', () => {
+ expect(isEmojiOnSeparateLine('
😀
')).toBe(true);
+ });
+
+ it('should return true for emoji line with case-insensitive
tags', () => {
+ expect(isEmojiOnSeparateLine('
😀
')).toBe(true);
+ });
+
+ it('should return true for emoji with multiple characters inside', () => {
+ expect(isEmojiOnSeparateLine('👨👩👧👦')).toBe(true);
+ });
+
+ it('should return false for empty string', () => {
+ expect(isEmojiOnSeparateLine('')).toBe(false);
+ });
+
+ it('should return false for line with only
tags', () => {
+ expect(isEmojiOnSeparateLine('
')).toBe(false);
+ });
+
+ it('should return false for text before emoji', () => {
+ expect(isEmojiOnSeparateLine('hello 😀')).toBe(false);
+ });
+
+ it('should return false for text after emoji', () => {
+ expect(isEmojiOnSeparateLine('😀 world')).toBe(false);
+ });
+
+ it('should return false for unclosed emoji tag', () => {
+ expect(isEmojiOnSeparateLine('😀')).toBe(false);
+ });
+
+ it('should return false for plain text without emoji tags', () => {
+ expect(isEmojiOnSeparateLine('just some text')).toBe(false);
+ });
+
+ it('should return false for emoji without tags', () => {
+ expect(isEmojiOnSeparateLine('😀')).toBe(false);
+ });
+
+ it('should return true for emoji line with whitespace and
combined', () => {
+ expect(isEmojiOnSeparateLine('
😀
')).toBe(true);
+ });
+});