Skip to content

Commit

Permalink
Merge pull request #40692 from kbieganowski/feat/enlarge-emojis-font-…
Browse files Browse the repository at this point in the history
…size

Emojis larger in other contexts than just single character messages
  • Loading branch information
roryabraham committed Jul 27, 2024
2 parents 9901ca3 + 08e72ff commit 5e65276
Show file tree
Hide file tree
Showing 21 changed files with 391 additions and 82 deletions.
4 changes: 2 additions & 2 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2305,8 +2305,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,

Expand Down
9 changes: 6 additions & 3 deletions src/components/Composer/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ function Composer(
) {
const textInput = useRef<AnimatedMarkdownTextInputRef | null>(null);
const {isFocused, shouldResetFocusRef} = useResetComposerFocus(textInput);
const textContainsOnlyEmojis = useMemo(() => EmojiUtils.containsOnlyEmojis(value ?? ''), [value]);
const doesTextContainOnlyEmojis = useMemo(() => EmojiUtils.containsOnlyEmojis(value ?? ''), [value]);
const theme = useTheme();
const markdownStyle = useMarkdownStyle(value, !isGroupPolicyReport ? excludeReportMentionStyle : excludeNoStyles);
const markdownStyle = useMarkdownStyle(doesTextContainOnlyEmojis, !isGroupPolicyReport ? excludeReportMentionStyle : excludeNoStyles);
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();

Expand Down Expand Up @@ -73,7 +73,10 @@ function Composer(
}, [shouldClear, onClear]);

const maxHeightStyle = useMemo(() => StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize), [StyleUtils, isComposerFullSize, maxLines]);
const composerStyle = useMemo(() => StyleSheet.flatten([style, textContainsOnlyEmojis ? styles.onlyEmojisTextLineHeight : {}]), [style, textContainsOnlyEmojis, styles]);
const composerStyle = useMemo(
() => StyleSheet.flatten([style, doesTextContainOnlyEmojis ? styles.onlyEmojisTextLineHeight : styles.emojisWithTextLineHeight]),
[style, doesTextContainOnlyEmojis, styles],
);

return (
<RNMarkdownTextInput
Expand Down
7 changes: 3 additions & 4 deletions src/components/Composer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@ function Composer(
}: ComposerProps,
ref: ForwardedRef<TextInput | HTMLInputElement>,
) {
const textContainsOnlyEmojis = useMemo(() => EmojiUtils.containsOnlyEmojis(value ?? ''), [value]);
const doesTextContainOnlyEmojis = useMemo(() => EmojiUtils.containsOnlyEmojis(value ?? ''), [value]);
const theme = useTheme();
const styles = useThemeStyles();
const markdownStyle = useMarkdownStyle(value, !isGroupPolicyReport ? excludeReportMentionStyle : excludeNoStyles);
const markdownStyle = useMarkdownStyle(doesTextContainOnlyEmojis, !isGroupPolicyReport ? excludeReportMentionStyle : excludeNoStyles);
const StyleUtils = useStyleUtils();
const textRef = useRef<HTMLElement & RNText>(null);
const textInput = useRef<AnimatedMarkdownTextInputRef | null>(null);
Expand Down Expand Up @@ -345,10 +345,9 @@ function Composer(
scrollStyleMemo,
StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize),
isComposerFullSize ? ({height: '100%', maxHeight: 'none' as DimensionValue} as TextStyle) : undefined,
textContainsOnlyEmojis ? styles.onlyEmojisTextLineHeight : {},
],

[style, styles.rtlTextRenderForSafari, styles.onlyEmojisTextLineHeight, scrollStyleMemo, StyleUtils, maxLines, isComposerFullSize, textContainsOnlyEmojis],
[style, styles.rtlTextRenderForSafari, scrollStyleMemo, StyleUtils, maxLines, isComposerFullSize],
);

return (
Expand Down
3 changes: 2 additions & 1 deletion src/components/InlineCodeBlock/WrappedText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ function getTextMatrix(text: string): string[][] {
* Validates if the text contains any emoji
*/
function containsEmoji(text: string): boolean {
return CONST.REGEX.EMOJIS.test(text);
const emojisRegex = new RegExp(CONST.REGEX.EMOJIS, CONST.REGEX.EMOJIS.flags.concat('g'));
return emojisRegex.test(text);
}

function WrappedText({children, wordStyles, textStyles}: WrappedTextProps) {
Expand Down
2 changes: 1 addition & 1 deletion src/components/SelectionList/Search/UserInfoCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function UserInfoCell({participant, displayName}: UserInfoCellProps) {
/>
<Text
numberOfLines={1}
style={[isLargeScreenWidth ? styles.themeTextColor : [styles.textMicro, styles.textBold], styles.flexShrink1]}
style={[isLargeScreenWidth ? styles.themeTextColor : styles.textMicroBold, styles.flexShrink1]}
>
{displayName}
</Text>
Expand Down
2 changes: 2 additions & 0 deletions src/components/SelectionList/UserListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import BaseListItem from './BaseListItem';
import type {ListItem, UserListItemProps} from './types';
Expand Down Expand Up @@ -131,6 +132,7 @@ function UserListItem<TItem extends ListItem>({
{!!item.alternateText && (
<TextWithTooltip
shouldShowTooltip={showTooltip}
emojiFontSize={variables.emojiSizeSmall}
text={Str.removeSMSDomain(item.alternateText ?? '')}
style={[styles.textLabelSupporting, styles.lh16, styles.pre]}
/>
Expand Down
1 change: 1 addition & 0 deletions src/components/TextInput/BaseTextInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ function BaseTextInput(
// Add disabled color theme when field is not editable.
inputProps.disabled && styles.textInputDisabled,
styles.pointerEventsAuto,
isMarkdownEnabled ? {lineHeight: variables.lineHeightMarkdownEnabledInput} : null,
]}
multiline={isMultiline}
maxLength={maxLength}
Expand Down
31 changes: 31 additions & 0 deletions src/components/TextWithTooltip/index.ios.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React, {useMemo} from 'react';
import Text from '@components/Text';
import * as EmojiUtils from '@libs/EmojiUtils';
import CONST from '@src/CONST';
import type TextWithTooltipProps from './types';

function TextWithTooltip({text, style, emojiFontSize, numberOfLines = 1}: TextWithTooltipProps) {
const processedTextArray = useMemo(() => {
const emojisRegex = new RegExp(CONST.REGEX.EMOJIS, CONST.REGEX.EMOJIS.flags.concat('g'));
const doesTextContainEmojis = !!(emojiFontSize && emojisRegex.test(text));

if (!doesTextContainEmojis) {
return [];
}

return EmojiUtils.splitTextWithEmojis(text);
}, [emojiFontSize, text]);

return (
<Text
style={style}
numberOfLines={numberOfLines}
>
{processedTextArray.length !== 0 ? processedTextArray.map(({text: textItem, isEmoji}) => (isEmoji ? <Text style={{fontSize: emojiFontSize}}>{textItem}</Text> : textItem)) : text}
</Text>
);
}

TextWithTooltip.displayName = 'TextWithTooltip';

export default TextWithTooltip;
3 changes: 3 additions & 0 deletions src/components/TextWithTooltip/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ type TextWithTooltipProps = {

/** Custom number of lines for text wrapping */
numberOfLines?: number;

/** Emoji font size */
emojiFontSize?: number;
};

export default TextWithTooltipProps;
9 changes: 3 additions & 6 deletions src/hooks/useMarkdownStyle.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import type {MarkdownStyle} from '@expensify/react-native-live-markdown';
import {useMemo} from 'react';
import {containsOnlyEmojis} from '@libs/EmojiUtils';
import FontUtils from '@styles/utils/FontUtils';
import variables from '@styles/variables';
import useTheme from './useTheme';

const defaultEmptyArray: Array<keyof MarkdownStyle> = [];

function useMarkdownStyle(message: string | null = null, excludeStyles: Array<keyof MarkdownStyle> = defaultEmptyArray): MarkdownStyle {
function useMarkdownStyle(doesInputContainOnlyEmojis?: boolean, excludeStyles: Array<keyof MarkdownStyle> = defaultEmptyArray): MarkdownStyle {
const theme = useTheme();
const hasMessageOnlyEmojis = message != null && message.length > 0 && containsOnlyEmojis(message);
const emojiFontSize = hasMessageOnlyEmojis ? variables.fontSizeOnlyEmojis : variables.fontSizeNormal;

// this map is used to reset the styles that are not needed - passing undefined value can break the native side
const nonStylingDefaultValues: Record<string, string | number> = useMemo(
Expand All @@ -37,7 +34,7 @@ function useMarkdownStyle(message: string | null = null, excludeStyles: Array<ke
fontSize: variables.fontSizeLarge,
},
emoji: {
fontSize: emojiFontSize,
fontSize: doesInputContainOnlyEmojis ? variables.fontSizeEmojisOnlyComposer : variables.fontSizeEmojisWithinText,
},
blockquote: {
borderColor: theme.border,
Expand Down Expand Up @@ -89,7 +86,7 @@ function useMarkdownStyle(message: string | null = null, excludeStyles: Array<ke
}

return styling;
}, [theme, emojiFontSize, excludeStyles, nonStylingDefaultValues]);
}, [theme, doesInputContainOnlyEmojis, excludeStyles, nonStylingDefaultValues]);

return markdownStyle;
}
Expand Down
62 changes: 58 additions & 4 deletions src/libs/EmojiUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ 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 emojisRegex = new RegExp(CONST.REGEX.EMOJIS, CONST.REGEX.EMOJIS.flags.concat('g'));
const match = trimmedMessage.match(emojisRegex);

if (!match) {
return false;
Expand All @@ -162,7 +163,8 @@ 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 emojisRegex = new RegExp(CONST.REGEX.EMOJIS, CONST.REGEX.EMOJIS.flags.concat('g'));
const match = trimmedMessage.match(emojisRegex);

if (!match) {
return false;
Expand Down Expand Up @@ -285,7 +287,8 @@ function extractEmojis(text: string): Emoji[] {
}

// Parse Emojis including skin tones - Eg: ['👩🏻', '👩🏻', '👩🏼', '👩🏻', '👩🏼', '👩']
const parsedEmojis = text.match(CONST.REGEX.EMOJIS);
const emojisRegex = new RegExp(CONST.REGEX.EMOJIS, CONST.REGEX.EMOJIS.flags.concat('g'));
const parsedEmojis = text.match(emojisRegex);

if (!parsedEmojis) {
return [];
Expand Down Expand Up @@ -586,7 +589,57 @@ function getSpacersIndexes(allEmojis: EmojiPickerList): number[] {
return spacersIndexes;
}

export type {HeaderIndice, EmojiPickerList, EmojiSpacer, EmojiPickerListItem};
type TextWithEmoji = {
text: string;
isEmoji: boolean;
};

function splitTextWithEmojis(text = ''): TextWithEmoji[] {
if (!text) {
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 a 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;
}

export type {HeaderIndice, EmojiPickerList, EmojiSpacer, EmojiPickerListItem, TextWithEmoji};

export {
findEmojiByName,
Expand All @@ -611,4 +664,5 @@ export {
hasAccountIDEmojiReacted,
getRemovedSkinToneEmoji,
getSpacersIndexes,
splitTextWithEmojis,
};
7 changes: 5 additions & 2 deletions src/libs/ValidationUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ function isValidAddress(value: FormValue): boolean {
return false;
}

if (!CONST.REGEX.ANY_VALUE.test(value) || value.match(CONST.REGEX.EMOJIS)) {
const emojisRegex = new RegExp(CONST.REGEX.EMOJIS, CONST.REGEX.EMOJIS.flags.concat('g'));

if (!CONST.REGEX.ANY_VALUE.test(value) || value.match(emojisRegex)) {
return false;
}

Expand Down Expand Up @@ -331,7 +333,8 @@ 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);
const emojisRegex = new RegExp(CONST.REGEX.EMOJIS, CONST.REGEX.EMOJIS.flags.concat('g'));
return !name.match(emojisRegex);
}

function isValidReportName(name: string) {
Expand Down
17 changes: 6 additions & 11 deletions src/pages/home/report/ReportActionItemFragment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 */
Expand Down Expand Up @@ -159,18 +159,13 @@ function ReportActionItemFragment({
}

return (
<UserDetailsTooltip
<ReportActionItemMessageHeaderSender
accountID={accountID}
delegateAccountID={delegateAccountID}
icon={actorIcon}
>
<Text
numberOfLines={isSingleLine ? 1 : undefined}
style={[styles.chatItemMessageHeaderSender, isSingleLine ? styles.pre : styles.preWrap]}
>
{fragment?.text}
</Text>
</UserDetailsTooltip>
fragmentText={fragment.text}
actorIcon={actorIcon}
isSingleLine={isSingleLine}
/>
);
}
case 'LINK':
Expand Down
58 changes: 58 additions & 0 deletions src/pages/home/report/ReportActionItemMessageHeaderSender.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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 CONST from '@src/CONST';
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;
};

function ReportActionItemMessageHeaderSender({fragmentText, accountID, delegateAccountID, actorIcon, isSingleLine}: ReportActionItemMessageHeaderSenderProps) {
const styles = useThemeStyles();

const processedTextArray = useMemo(() => {
const emojisRegex = new RegExp(CONST.REGEX.EMOJIS, CONST.REGEX.EMOJIS.flags.concat('g'));
const doesTextContainEmojis = emojisRegex.test(fragmentText);

if (!doesTextContainEmojis) {
return [];
}

return EmojiUtils.splitTextWithEmojis(fragmentText);
}, [fragmentText]);

return (
<UserDetailsTooltip
accountID={accountID}
delegateAccountID={delegateAccountID}
icon={actorIcon}
>
<Text
numberOfLines={isSingleLine ? 1 : undefined}
style={[styles.chatItemMessageHeaderSender, isSingleLine ? styles.pre : styles.preWrap]}
>
{processedTextArray.length !== 0 ? processedTextArray.map(({text, isEmoji}) => (isEmoji ? <Text style={styles.emojisWithinDisplayName}>{text}</Text> : text)) : fragmentText}
</Text>
</UserDetailsTooltip>
);
}

ReportActionItemMessageHeaderSender.displayName = 'ReportActionItemMessageHeaderSender';

export default ReportActionItemMessageHeaderSender;
Loading

0 comments on commit 5e65276

Please sign in to comment.