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); + }); +});