diff --git a/src/CONST.ts b/src/CONST.ts index 7c8a6791d65b..4376594342f8 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2932,8 +2932,8 @@ const CONST = { // eslint-disable-next-line max-len, no-misleading-character-class EMOJI: /[\p{Extended_Pictographic}\u200d\u{1f1e6}-\u{1f1ff}\u{1f3fb}-\u{1f3ff}\u{e0020}-\u{e007f}\u20E3\uFE0F]|[#*0-9]\uFE0F?\u20E3/gu, - // eslint-disable-next-line max-len, no-misleading-character-class - EMOJIS: /[\p{Extended_Pictographic}](\u200D[\p{Extended_Pictographic}]|[\u{1F3FB}-\u{1F3FF}]|[\u{E0020}-\u{E007F}]|\uFE0F|\u20E3)*|[\u{1F1E6}-\u{1F1FF}]{2}|[#*0-9]\uFE0F?\u20E3/gu, + // eslint-disable-next-line max-len, no-misleading-character-class, no-empty-character-class + EMOJIS: /[\p{Extended_Pictographic}](\u200D[\p{Extended_Pictographic}]|[\u{1F3FB}-\u{1F3FF}]|[\u{E0020}-\u{E007F}]|\uFE0F|\u20E3)*|[\u{1F1E6}-\u{1F1FF}]{2}|[#*0-9]\uFE0F?\u20E3/du, // eslint-disable-next-line max-len, no-misleading-character-class EMOJI_SKIN_TONES: /[\u{1f3fb}-\u{1f3ff}]/gu, @@ -2970,6 +2970,10 @@ const CONST = { return new RegExp(`[\\n\\s]|${this.SPECIAL_CHAR.source}|${this.EMOJI.source}`, 'gu'); }, + get ALL_EMOJIS() { + return new RegExp(this.EMOJIS, this.EMOJIS.flags.concat('g')); + }, + MERGED_ACCOUNT_PREFIX: /^(MERGED_\d+@)/, ROUTES: { VALIDATE_LOGIN: /\/v($|(\/\/*))/, diff --git a/src/components/AccountSwitcher.tsx b/src/components/AccountSwitcher.tsx index 9a90de17595d..ed2eae7a0a4c 100644 --- a/src/components/AccountSwitcher.tsx +++ b/src/components/AccountSwitcher.tsx @@ -10,6 +10,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {clearDelegatorErrors, connect, disconnect} from '@libs/actions/Delegate'; +import * as EmojiUtils from '@libs/EmojiUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import variables from '@styles/variables'; @@ -46,6 +47,7 @@ function AccountSwitcher() { const isActingAsDelegate = !!account?.delegatedAccess?.delegate ?? false; const canSwitchAccounts = delegators.length > 0 || isActingAsDelegate; + const processedTextArray = EmojiUtils.splitTextWithEmojis(currentUserPersonalDetails?.displayName); const createBaseMenuItem = ( personalDetails: PersonalDetails | undefined, @@ -149,7 +151,9 @@ function AccountSwitcher() { numberOfLines={1} style={[styles.textBold, styles.textLarge, styles.flexShrink1]} > - {currentUserPersonalDetails?.displayName} + {processedTextArray.length !== 0 + ? EmojiUtils.getProcessedText(processedTextArray, styles.initialSettingsUsernameEmoji) + : currentUserPersonalDetails?.displayName} {!!canSwitchAccounts && ( diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx index be875790d75e..e71ade65e66d 100755 --- a/src/components/Composer/implementation/index.tsx +++ b/src/components/Composer/implementation/index.tsx @@ -19,6 +19,7 @@ import * as Browser from '@libs/Browser'; import * as EmojiUtils from '@libs/EmojiUtils'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; const excludeNoStyles: Array = []; @@ -70,6 +71,7 @@ function Composer( start: selectionProp.start, end: selectionProp.end, }); + const [hasMultipleLines, setHasMultipleLines] = useState(false); const [isRendered, setIsRendered] = useState(false); const isScrollBarVisible = useIsScrollBarVisible(textInput, value ?? ''); const [prevScroll, setPrevScroll] = useState(); @@ -328,10 +330,10 @@ function Composer( scrollStyleMemo, StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize), isComposerFullSize ? {height: '100%', maxHeight: 'none'} : undefined, - textContainsOnlyEmojis ? styles.onlyEmojisTextLineHeight : {}, + textContainsOnlyEmojis && hasMultipleLines ? styles.onlyEmojisTextLineHeight : {}, ], - [style, styles.rtlTextRenderForSafari, styles.onlyEmojisTextLineHeight, scrollStyleMemo, StyleUtils, maxLines, isComposerFullSize, textContainsOnlyEmojis], + [style, styles.rtlTextRenderForSafari, styles.onlyEmojisTextLineHeight, scrollStyleMemo, hasMultipleLines, StyleUtils, maxLines, isComposerFullSize, textContainsOnlyEmojis], ); return ( @@ -350,6 +352,9 @@ function Composer( /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} onSelectionChange={addCursorPositionToSelectionChange} + onContentSizeChange={(e) => { + setHasMultipleLines(e.nativeEvent.contentSize.height > variables.componentSizeLarge); + }} disabled={isDisabled} onKeyPress={handleKeyPress} addAuthTokenToImageURLCallback={addEncryptedAuthTokenToURL} diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx index 31d092800d20..879684210825 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx @@ -1,11 +1,22 @@ -import React from 'react'; +import React, {useMemo} from 'react'; +import type {TextStyle} from 'react-native'; import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-html'; import EmojiWithTooltip from '@components/EmojiWithTooltip'; import useThemeStyles from '@hooks/useThemeStyles'; function EmojiRenderer({tnode, style: styleProp}: CustomRendererProps) { const styles = useThemeStyles(); - const style = {...styleProp, ...('islarge' in tnode.attributes ? styles.onlyEmojisText : {})}; + 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]; + } + + return null; + }, [tnode.attributes, styles, styleProp]); return ( {displayName} diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 670126f8c6ec..127989b5faa0 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -182,9 +182,10 @@ function BaseTextInput( } const layout = event.nativeEvent.layout; + const HEIGHT_TO_FIT_EMOJIS = 1; setWidth((prevWidth: number | null) => (autoGrowHeight ? layout.width : prevWidth)); - setHeight((prevHeight: number) => (!multiline ? layout.height : prevHeight)); + setHeight((prevHeight: number) => (!multiline ? layout.height + HEIGHT_TO_FIT_EMOJIS : prevHeight)); }, [autoGrowHeight, multiline], ); diff --git a/src/components/TextWithTooltip/index.native.tsx b/src/components/TextWithTooltip/index.native.tsx index b857ded2588b..9f5f246ff9d3 100644 --- a/src/components/TextWithTooltip/index.native.tsx +++ b/src/components/TextWithTooltip/index.native.tsx @@ -1,14 +1,19 @@ import React from 'react'; import Text from '@components/Text'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as EmojiUtils from '@libs/EmojiUtils'; import type TextWithTooltipProps from './types'; function TextWithTooltip({text, style, numberOfLines = 1}: TextWithTooltipProps) { + const styles = useThemeStyles(); + const processedTextArray = EmojiUtils.splitTextWithEmojis(text); + return ( - {text} + {processedTextArray.length !== 0 ? EmojiUtils.getProcessedText(processedTextArray, [style, styles.emojisFontFamily]) : text} ); } diff --git a/src/components/WorkspacesListRowDisplayName/index.native.tsx b/src/components/WorkspacesListRowDisplayName/index.native.tsx new file mode 100644 index 000000000000..1a91e2857db3 --- /dev/null +++ b/src/components/WorkspacesListRowDisplayName/index.native.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import Text from '@components/Text'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as EmojiUtils from '@libs/EmojiUtils'; +import type WorkspacesListRowDisplayNameProps from './types'; + +function WorkspacesListRowDisplayName({isDeleted, ownerName}: WorkspacesListRowDisplayNameProps) { + const styles = useThemeStyles(); + const processedOwnerName = EmojiUtils.splitTextWithEmojis(ownerName); + + return ( + + {processedOwnerName.length !== 0 + ? EmojiUtils.getProcessedText(processedOwnerName, [styles.labelStrong, isDeleted ? styles.offlineFeedback.deleted : {}, styles.emojisWithTextFontFamily]) + : ownerName} + + ); +} + +WorkspacesListRowDisplayName.displayName = 'WorkspacesListRowDisplayName'; + +export default WorkspacesListRowDisplayName; diff --git a/src/components/WorkspacesListRowDisplayName/index.tsx b/src/components/WorkspacesListRowDisplayName/index.tsx new file mode 100644 index 000000000000..0d3acb736d2f --- /dev/null +++ b/src/components/WorkspacesListRowDisplayName/index.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import Text from '@components/Text'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type WorkspacesListRowDisplayNameProps from './types'; + +function WorkspacesListRowDisplayName({isDeleted, ownerName}: WorkspacesListRowDisplayNameProps) { + const styles = useThemeStyles(); + + return ( + + {ownerName} + + ); +} + +WorkspacesListRowDisplayName.displayName = 'WorkspacesListRowDisplayName'; + +export default WorkspacesListRowDisplayName; diff --git a/src/components/WorkspacesListRowDisplayName/types.tsx b/src/components/WorkspacesListRowDisplayName/types.tsx new file mode 100644 index 000000000000..0744ebc18fc1 --- /dev/null +++ b/src/components/WorkspacesListRowDisplayName/types.tsx @@ -0,0 +1,9 @@ +type WorkspacesListRowDisplayNameProps = { + /** Should the deleted style be applied */ + isDeleted: boolean; + + /** Workspace owner name */ + ownerName: string; +}; + +export default WorkspacesListRowDisplayNameProps; diff --git a/src/hooks/useMarkdownStyle.ts b/src/hooks/useMarkdownStyle.ts index 2006ca85dd13..7b38cc12347f 100644 --- a/src/hooks/useMarkdownStyle.ts +++ b/src/hooks/useMarkdownStyle.ts @@ -10,7 +10,7 @@ const defaultEmptyArray: Array = []; function useMarkdownStyle(message: string | null = null, excludeStyles: Array = defaultEmptyArray): MarkdownStyle { const theme = useTheme(); const hasMessageOnlyEmojis = message != null && message.length > 0 && containsOnlyEmojis(message); - const emojiFontSize = hasMessageOnlyEmojis ? variables.fontSizeOnlyEmojis : variables.fontSizeNormal; + const emojiFontSize = hasMessageOnlyEmojis ? variables.fontSizeOnlyEmojis : variables.fontSizeEmojisWithinText; // this map is used to reset the styles that are not needed - passing undefined value can break the native side const nonStylingDefaultValues: Record = useMemo( @@ -38,6 +38,7 @@ function useMarkdownStyle(message: string | null = null, excludeStyles: Array Emojis.emojiNameTable[name]; @@ -148,7 +155,7 @@ function trimEmojiUnicode(emojiCode: string): string { */ function isFirstLetterEmoji(message: string): boolean { const trimmedMessage = Str.replaceAll(message.replace(/ /g, ''), '\n', ''); - const match = trimmedMessage.match(CONST.REGEX.EMOJIS); + const match = trimmedMessage.match(CONST.REGEX.ALL_EMOJIS); if (!match) { return false; @@ -162,7 +169,7 @@ function isFirstLetterEmoji(message: string): boolean { */ function containsOnlyEmojis(message: string): boolean { const trimmedMessage = Str.replaceAll(message.replace(/ /g, ''), '\n', ''); - const match = trimmedMessage.match(CONST.REGEX.EMOJIS); + const match = trimmedMessage.match(CONST.REGEX.ALL_EMOJIS); if (!match) { return false; @@ -285,7 +292,7 @@ function extractEmojis(text: string): Emoji[] { } // Parse Emojis including skin tones - Eg: ['👩🏻', '👩🏻', '👩🏼', '👩🏻', '👩🏼', '👩'] - const parsedEmojis = text.match(CONST.REGEX.EMOJIS); + const parsedEmojis = text.match(CONST.REGEX.ALL_EMOJIS); if (!parsedEmojis) { return []; @@ -595,6 +602,75 @@ function getSpacersIndexes(allEmojis: EmojiPickerList): number[] { return spacersIndexes; } +/** Splits the text with emojis into array if emojis exist in the text */ +function splitTextWithEmojis(text = ''): TextWithEmoji[] { + if (!text) { + return []; + } + + const doesTextContainEmojis = new RegExp(CONST.REGEX.EMOJIS, CONST.REGEX.EMOJIS.flags.concat('g')).test(text); + + if (!doesTextContainEmojis) { + return []; + } + + // The regex needs to be cloned because `exec()` is a stateful operation and maintains the state inside + // the regex variable itself, so we must have an independent instance for each function's call. + const emojisRegex = new RegExp(CONST.REGEX.EMOJIS, CONST.REGEX.EMOJIS.flags.concat('g')); + + const splitText: TextWithEmoji[] = []; + let regexResult: RegExpExecArray | null; + let lastMatchIndexEnd = 0; + + do { + regexResult = emojisRegex.exec(text); + + if (regexResult?.indices) { + const matchIndexStart = regexResult.indices[0][0]; + const matchIndexEnd = regexResult.indices[0][1]; + + if (matchIndexStart > lastMatchIndexEnd) { + splitText.push({ + text: text.slice(lastMatchIndexEnd, matchIndexStart), + isEmoji: false, + }); + } + + splitText.push({ + text: text.slice(matchIndexStart, matchIndexEnd), + isEmoji: true, + }); + + lastMatchIndexEnd = matchIndexEnd; + } + } while (regexResult !== null); + + if (lastMatchIndexEnd < text.length) { + splitText.push({ + text: text.slice(lastMatchIndexEnd, text.length), + isEmoji: false, + }); + } + + return splitText; +} + +function getProcessedText(processedTextArray: TextWithEmoji[], style: StyleProp): Array { + return processedTextArray.map(({text, isEmoji}, index) => + isEmoji ? ( + + {text} + + ) : ( + text + ), + ); +} + export type {HeaderIndice, EmojiPickerList, EmojiSpacer, EmojiPickerListItem}; export { @@ -602,6 +678,7 @@ export { findEmojiByCode, getEmojiName, getLocalizedEmojiName, + getProcessedText, getHeaderEmojis, mergeEmojisWithFrequentlyUsedEmojis, containsOnlyEmojis, @@ -620,4 +697,5 @@ export { hasAccountIDEmojiReacted, getRemovedSkinToneEmoji, getSpacersIndexes, + splitTextWithEmojis, }; diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 0367325db6b1..9c49c5f2ced6 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -49,7 +49,7 @@ function isValidAddress(value: FormValue): boolean { return false; } - if (!CONST.REGEX.ANY_VALUE.test(value) || value.match(CONST.REGEX.EMOJIS)) { + if (!CONST.REGEX.ANY_VALUE.test(value) || value.match(CONST.REGEX.ALL_EMOJIS)) { return false; } @@ -343,7 +343,7 @@ function isValidRoutingNumber(routingNumber: string): boolean { * Checks that the provided name doesn't contain any emojis */ function isValidCompanyName(name: string) { - return !name.match(CONST.REGEX.EMOJIS); + return !name.match(CONST.REGEX.ALL_EMOJIS); } function isValidReportName(name: string) { diff --git a/src/pages/home/report/ReportActionItemFragment.tsx b/src/pages/home/report/ReportActionItemFragment.tsx index 787904d72b81..05cb657b1e54 100644 --- a/src/pages/home/report/ReportActionItemFragment.tsx +++ b/src/pages/home/report/ReportActionItemFragment.tsx @@ -2,7 +2,6 @@ import React, {memo} from 'react'; import type {StyleProp, TextStyle} from 'react-native'; import RenderHTML from '@components/RenderHTML'; import Text from '@components/Text'; -import UserDetailsTooltip from '@components/UserDetailsTooltip'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -15,6 +14,7 @@ import type {Message} from '@src/types/onyx/ReportAction'; import type ReportActionName from '@src/types/onyx/ReportActionName'; import AttachmentCommentFragment from './comment/AttachmentCommentFragment'; import TextCommentFragment from './comment/TextCommentFragment'; +import ReportActionItemMessageHeaderSender from './ReportActionItemMessageHeaderSender'; type ReportActionItemFragmentProps = { /** Users accountID */ @@ -160,18 +160,13 @@ function ReportActionItemFragment({ } return ( - - - {fragment?.text} - - + fragmentText={fragment.text} + actorIcon={actorIcon} + isSingleLine={isSingleLine} + /> ); } case 'LINK': diff --git a/src/pages/home/report/ReportActionItemMessageHeaderSender/index.native.tsx b/src/pages/home/report/ReportActionItemMessageHeaderSender/index.native.tsx new file mode 100644 index 000000000000..9a752c3a9007 --- /dev/null +++ b/src/pages/home/report/ReportActionItemMessageHeaderSender/index.native.tsx @@ -0,0 +1,30 @@ +import React, {useMemo} from 'react'; +import Text from '@components/Text'; +import UserDetailsTooltip from '@components/UserDetailsTooltip'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as EmojiUtils from '@libs/EmojiUtils'; +import type ReportActionItemMessageHeaderSenderProps from './types'; + +function ReportActionItemMessageHeaderSender({fragmentText, accountID, delegateAccountID, actorIcon, isSingleLine}: ReportActionItemMessageHeaderSenderProps) { + const styles = useThemeStyles(); + const processedTextArray = useMemo(() => EmojiUtils.splitTextWithEmojis(fragmentText), [fragmentText]); + + return ( + + + {processedTextArray.length !== 0 ? EmojiUtils.getProcessedText(processedTextArray, [styles.emojisWithTextFontSize, styles.emojisWithTextFontFamily]) : fragmentText} + + + ); +} + +ReportActionItemMessageHeaderSender.displayName = 'ReportActionItemMessageHeaderSender'; + +export default ReportActionItemMessageHeaderSender; diff --git a/src/pages/home/report/ReportActionItemMessageHeaderSender/index.tsx b/src/pages/home/report/ReportActionItemMessageHeaderSender/index.tsx new file mode 100644 index 000000000000..d5602dbedfae --- /dev/null +++ b/src/pages/home/report/ReportActionItemMessageHeaderSender/index.tsx @@ -0,0 +1,30 @@ +import React, {useMemo} from 'react'; +import Text from '@components/Text'; +import UserDetailsTooltip from '@components/UserDetailsTooltip'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as EmojiUtils from '@libs/EmojiUtils'; +import type ReportActionItemMessageHeaderSenderProps from './types'; + +function ReportActionItemMessageHeaderSender({fragmentText, accountID, delegateAccountID, actorIcon, isSingleLine}: ReportActionItemMessageHeaderSenderProps) { + const styles = useThemeStyles(); + const processedTextArray = useMemo(() => EmojiUtils.splitTextWithEmojis(fragmentText), [fragmentText]); + + return ( + + + {processedTextArray.length !== 0 ? EmojiUtils.getProcessedText(processedTextArray, styles.emojisWithTextFontSize) : fragmentText} + + + ); +} + +ReportActionItemMessageHeaderSender.displayName = 'ReportActionItemMessageHeaderSender'; + +export default ReportActionItemMessageHeaderSender; diff --git a/src/pages/home/report/ReportActionItemMessageHeaderSender/types.ts b/src/pages/home/report/ReportActionItemMessageHeaderSender/types.ts new file mode 100644 index 000000000000..44a27de119e6 --- /dev/null +++ b/src/pages/home/report/ReportActionItemMessageHeaderSender/types.ts @@ -0,0 +1,20 @@ +import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; + +type ReportActionItemMessageHeaderSenderProps = { + /** Text to display */ + fragmentText: string; + + /** Users accountID */ + accountID: number; + + /** Should this fragment be contained in a single line? */ + isSingleLine?: boolean; + + /** The accountID of the copilot who took this action on behalf of the user */ + delegateAccountID?: number; + + /** Actor icon */ + actorIcon?: OnyxCommon.Icon; +}; + +export default ReportActionItemMessageHeaderSenderProps; diff --git a/src/pages/home/report/comment/TextCommentFragment.tsx b/src/pages/home/report/comment/TextCommentFragment.tsx index ab06a594a17f..913de87af4ac 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} from 'react'; +import React, {memo, useEffect, useMemo} from 'react'; import type {StyleProp, TextStyle} from 'react-native'; import Text from '@components/Text'; import ZeroWidthView from '@components/ZeroWidthView'; @@ -20,6 +20,7 @@ import type {OriginalMessageSource} from '@src/types/onyx/OriginalMessage'; import type {Message} from '@src/types/onyx/ReportAction'; import RenderCommentHTML from './RenderCommentHTML'; import shouldRenderAsText from './shouldRenderAsText'; +import TextWithEmojiFragment from './TextWithEmojiFragment'; type TextCommentFragmentProps = { /** The reportAction's source */ @@ -52,6 +53,10 @@ function TextCommentFragment({fragment, styleAsDeleted, styleAsMuted = false, so const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const message = isEmpty(iouMessage) ? text : iouMessage; + + const processedTextArray = useMemo(() => EmojiUtils.splitTextWithEmojis(message), [message]); + useEffect(() => { Performance.markEnd(CONST.TIMING.SEND_MESSAGE, {message: text}); Timing.end(CONST.TIMING.SEND_MESSAGE); @@ -69,7 +74,10 @@ function TextCommentFragment({fragment, styleAsDeleted, styleAsMuted = false, so if (containsOnlyEmojis) { htmlContent = Str.replaceAll(htmlContent, '', ''); htmlContent = Str.replaceAll(htmlContent, '
', '
'); + } else if (CONST.REGEX.ALL_EMOJIS.test(text ?? '')) { + htmlContent = Str.replaceAll(htmlWithDeletedTag, '', ''); } + let htmlWithTag = editedTag ? `${htmlContent}${editedTag}` : htmlContent; if (styleAsMuted) { @@ -84,26 +92,37 @@ function TextCommentFragment({fragment, styleAsDeleted, styleAsMuted = false, so ); } - const message = isEmpty(iouMessage) ? text : iouMessage; - return ( - - {convertToLTR(message ?? '')} - + {processedTextArray.length !== 0 && !containsOnlyEmojis ? ( + + ) : ( + + {convertToLTR(message ?? '')} + + )} {!!fragment?.isEdited && ( <> EmojiUtils.splitTextWithEmojis(message), [message]); + + return ( + + {processedTextArray.map(({text, isEmoji}, index) => + isEmoji ? ( + + {text} + + ) : ( + convertToLTR(text) + ), + )} + + ); +} + +TextWithEmojiFragment.displayName = 'TextWithEmojiFragment'; + +export default TextWithEmojiFragment; diff --git a/src/pages/home/report/comment/TextWithEmojiFragment/index.tsx b/src/pages/home/report/comment/TextWithEmojiFragment/index.tsx new file mode 100644 index 000000000000..d19725da766d --- /dev/null +++ b/src/pages/home/report/comment/TextWithEmojiFragment/index.tsx @@ -0,0 +1,33 @@ +import React, {useMemo} from 'react'; +import Text from '@components/Text'; +import useThemeStyles from '@hooks/useThemeStyles'; +import convertToLTR from '@libs/convertToLTR'; +import * as EmojiUtils from '@libs/EmojiUtils'; +import type TextWithEmojiFragmentProps from './types'; + +function TextWithEmojiFragment({message = '', style}: TextWithEmojiFragmentProps) { + const styles = useThemeStyles(); + const processedTextArray = useMemo(() => EmojiUtils.splitTextWithEmojis(message), [message]); + + return ( + + {processedTextArray.map(({text, isEmoji}, index) => + isEmoji ? ( + + {text} + + ) : ( + convertToLTR(text) + ), + )} + + ); +} + +TextWithEmojiFragment.displayName = 'TextWithEmojiFragment'; + +export default TextWithEmojiFragment; diff --git a/src/pages/home/report/comment/TextWithEmojiFragment/types.ts b/src/pages/home/report/comment/TextWithEmojiFragment/types.ts new file mode 100644 index 000000000000..243b02f1fd76 --- /dev/null +++ b/src/pages/home/report/comment/TextWithEmojiFragment/types.ts @@ -0,0 +1,11 @@ +import type {StyleProp, TextStyle} from 'react-native'; + +type TextWithEmojiFragmentProps = { + /** The message to be displayed */ + message?: string; + + /** Any additional styles to apply */ + style: StyleProp; +}; + +export default TextWithEmojiFragmentProps; diff --git a/src/pages/settings/Profile/DisplayNamePage.tsx b/src/pages/settings/Profile/DisplayNamePage.tsx index 90f7ca3abbd6..4c6211bc3e37 100644 --- a/src/pages/settings/Profile/DisplayNamePage.tsx +++ b/src/pages/settings/Profile/DisplayNamePage.tsx @@ -1,7 +1,6 @@ import React from 'react'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; @@ -22,11 +21,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/DisplayNameForm'; -type DisplayNamePageOnyxProps = { - isLoadingApp: OnyxEntry; -}; - -type DisplayNamePageProps = DisplayNamePageOnyxProps & WithCurrentUserPersonalDetailsProps; +type DisplayNamePageProps = WithCurrentUserPersonalDetailsProps; /** * Submit form to update user's first and last name (and display name) @@ -36,9 +31,10 @@ const updateDisplayName = (values: FormOnyxValues @@ -114,6 +111,7 @@ function DisplayNamePage({isLoadingApp = true, currentUserPersonalDetails}: Disp role={CONST.ROLE.PRESENTATION} defaultValue={currentUserDetails.lastName ?? ''} spellCheck={false} + isMarkdownEnabled /> @@ -124,10 +122,4 @@ function DisplayNamePage({isLoadingApp = true, currentUserPersonalDetails}: Disp DisplayNamePage.displayName = 'DisplayNamePage'; -export default withCurrentUserPersonalDetails( - withOnyx({ - isLoadingApp: { - key: ONYXKEYS.IS_LOADING_APP, - }, - })(DisplayNamePage), -); +export default withCurrentUserPersonalDetails(DisplayNamePage); diff --git a/src/pages/workspace/WorkspacesListRow.tsx b/src/pages/workspace/WorkspacesListRow.tsx index e202baf9b39d..5569d4fb3d70 100644 --- a/src/pages/workspace/WorkspacesListRow.tsx +++ b/src/pages/workspace/WorkspacesListRow.tsx @@ -12,6 +12,7 @@ import Text from '@components/Text'; import ThreeDotsMenu from '@components/ThreeDotsMenu'; import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import WorkspacesListRowDisplayName from '@components/WorkspacesListRowDisplayName'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; @@ -213,12 +214,10 @@ function WorkspacesListRow({ containerStyles={styles.workspaceOwnerAvatarWrapper} /> - - {PersonalDetailsUtils.getDisplayNameOrDefault(ownerDetails)} - + textAlign: 'left', }, + verticalAlignTopText: { + verticalAlign: 'text-top', + }, verticalAlignTop: { verticalAlign: 'top', }, @@ -415,7 +418,7 @@ const styles = (theme: ThemeColors) => color: theme.text, ...FontUtils.fontFamily.platform.EXP_NEUE_BOLD, fontSize: variables.fontSizeSmall, - lineHeight: variables.lineHeightSmall, + lineHeight: variables.lineHeightNormal, }, textMicroSupporting: { @@ -1746,6 +1749,31 @@ const styles = (theme: ThemeColors) => lineHeight: variables.fontSizeOnlyEmojisHeight, }, + emojisWithTextFontSizeAligned: { + fontSize: variables.fontSizeEmojisWithinText, + marginVertical: -7, + }, + + emojisFontFamily: { + fontFamily: FontUtils.fontFamily.platform.SYSTEM.fontFamily, + }, + + emojisWithTextFontSize: { + fontSize: variables.fontSizeEmojisWithinText, + }, + + emojisWithTextFontFamily: { + fontFamily: FontUtils.fontFamily.platform.SYSTEM.fontFamily, + }, + + emojisWithTextLineHeight: { + lineHeight: variables.lineHeightXLarge, + }, + + initialSettingsUsernameEmoji: { + fontSize: variables.fontSizeUsernameEmoji, + }, + createMenuPositionSidebar: (windowHeight: number) => ({ horizontal: 18, diff --git a/src/styles/utils/emojiDefaultStyles/index.ts b/src/styles/utils/emojiDefaultStyles/index.ts index 88c42e7e95d1..45880b46005d 100644 --- a/src/styles/utils/emojiDefaultStyles/index.ts +++ b/src/styles/utils/emojiDefaultStyles/index.ts @@ -6,7 +6,7 @@ import type EmojiDefaultStyles from './types'; const emojiDefaultStyles: EmojiDefaultStyles = { fontStyle: 'normal', fontWeight: FontUtils.fontWeight.normal, - ...display.dInlineFlex, + ...display.dInline, }; export default emojiDefaultStyles; diff --git a/src/styles/variables.ts b/src/styles/variables.ts index 5a8927ede6d0..8318b58012a9 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -48,8 +48,6 @@ export default { defaultAvatarPreviewSize: 360, fabBottom: 25, breadcrumbsFontSize: getValueUsingPixelRatio(19, 32), - fontSizeOnlyEmojis: 30, - fontSizeOnlyEmojisHeight: 35, fontSizeSmall: getValueUsingPixelRatio(11, 17), fontSizeExtraSmall: 9, fontSizeLabel: getValueUsingPixelRatio(13, 19), @@ -89,8 +87,6 @@ export default { sidebarAvatarSize: 28, iconHeader: 48, iconSection: 68, - emojiSize: 20, - emojiLineHeight: 28, iouAmountTextSize: 40, extraSmallMobileResponsiveWidthBreakpoint: 320, extraSmallMobileResponsiveHeightBreakpoint: 667, @@ -218,6 +214,14 @@ export default { onboardingModalWidth: 500, fontSizeToWidthRatio: getValueUsingPixelRatio(0.8, 1), + // Emoji related variables + fontSizeOnlyEmojis: 30, + fontSizeOnlyEmojisHeight: 35, + emojiSize: 20, + emojiLineHeight: 28, + fontSizeUsernameEmoji: 19, + fontSizeEmojisWithinText: getValueUsingPixelRatio(17, 19), + // The height of the empty list is 14px (2px for borders and 12px for vertical padding) // This is calculated based on the values specified in the 'getGoogleListViewStyle' function of the 'StyleUtils' utility googleEmptyListViewHeight: 14,