Skip to content

Commit

Permalink
Merge pull request #47547 from VickyStash/feature/large-emojis
Browse files Browse the repository at this point in the history
Enlarge emojis in other contexts than just single character messages
  • Loading branch information
francoisl authored Nov 21, 2024
2 parents f84785d + fb197bb commit fd3639a
Show file tree
Hide file tree
Showing 28 changed files with 484 additions and 70 deletions.
8 changes: 6 additions & 2 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2969,8 +2969,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 Expand Up @@ -3007,6 +3007,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($|(\/\/*))/,
Expand Down
6 changes: 5 additions & 1 deletion src/components/AccountSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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}
</Text>
{!!canSwitchAccounts && (
<View style={styles.justifyContentCenter}>
Expand Down
9 changes: 7 additions & 2 deletions src/components/Composer/implementation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<keyof MarkdownStyle> = [];
Expand Down Expand Up @@ -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<number | undefined>();
Expand Down Expand Up @@ -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 (
Expand All @@ -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}
Expand Down
15 changes: 13 additions & 2 deletions src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx
Original file line number Diff line number Diff line change
@@ -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<TText | TPhrasing>) {
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 (
<EmojiWithTooltip
style={[style, styles.cursorDefault, styles.emojiDefaultStyles]}
Expand Down
2 changes: 1 addition & 1 deletion src/components/InlineCodeBlock/WrappedText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function getTextMatrix(text: string): string[][] {
* Validates if the text contains any emoji
*/
function containsEmoji(text: string): boolean {
return CONST.REGEX.EMOJIS.test(text);
return CONST.REGEX.ALL_EMOJIS.test(text);
}

/**
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 @@ -35,7 +35,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
5 changes: 4 additions & 1 deletion src/components/TextInput/BaseTextInput/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,11 @@ function BaseTextInput(

const layout = event.nativeEvent.layout;

// We need to increase the height for single line inputs to escape cursor jumping on ios
const heightToFitEmojis = 1;

setWidth((prevWidth: number | null) => (autoGrowHeight ? layout.width : prevWidth));
setHeight((prevHeight: number) => (!multiline ? layout.height : prevHeight));
setHeight((prevHeight: number) => (!multiline ? layout.height + heightToFitEmojis : prevHeight));
},
[autoGrowHeight, multiline],
);
Expand Down
7 changes: 6 additions & 1 deletion src/components/TextWithTooltip/index.native.tsx
Original file line number Diff line number Diff line change
@@ -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
style={style}
numberOfLines={numberOfLines}
>
{text}
{processedTextArray.length !== 0 ? EmojiUtils.getProcessedText(processedTextArray, [style, styles.emojisFontFamily]) : text}
</Text>
);
}
Expand Down
25 changes: 25 additions & 0 deletions src/components/WorkspacesListRowDisplayName/index.native.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Text
numberOfLines={1}
style={[styles.labelStrong, isDeleted ? styles.offlineFeedback.deleted : {}]}
>
{processedOwnerName.length !== 0
? EmojiUtils.getProcessedText(processedOwnerName, [styles.labelStrong, isDeleted ? styles.offlineFeedback.deleted : {}, styles.emojisWithTextFontFamily])
: ownerName}
</Text>
);
}

WorkspacesListRowDisplayName.displayName = 'WorkspacesListRowDisplayName';

export default WorkspacesListRowDisplayName;
21 changes: 21 additions & 0 deletions src/components/WorkspacesListRowDisplayName/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Text
numberOfLines={1}
style={[styles.labelStrong, isDeleted ? styles.offlineFeedback.deleted : {}]}
>
{ownerName}
</Text>
);
}

WorkspacesListRowDisplayName.displayName = 'WorkspacesListRowDisplayName';

export default WorkspacesListRowDisplayName;
9 changes: 9 additions & 0 deletions src/components/WorkspacesListRowDisplayName/types.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
type WorkspacesListRowDisplayNameProps = {
/** Should the deleted style be applied */
isDeleted: boolean;

/** Workspace owner name */
ownerName: string;
};

export default WorkspacesListRowDisplayNameProps;
3 changes: 2 additions & 1 deletion src/hooks/useMarkdownStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const defaultEmptyArray: Array<keyof MarkdownStyle> = [];
function useMarkdownStyle(message: string | null = null, 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;
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<string, string | number> = useMemo(
Expand Down Expand Up @@ -38,6 +38,7 @@ function useMarkdownStyle(message: string | null = null, excludeStyles: Array<ke
},
emoji: {
fontSize: emojiFontSize,
lineHeight: variables.lineHeightXLarge,
},
blockquote: {
borderColor: theme.border,
Expand Down
84 changes: 81 additions & 3 deletions src/libs/EmojiUtils.ts → src/libs/EmojiUtils.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import {Str} from 'expensify-common';
import lodashSortBy from 'lodash/sortBy';
import React from 'react';
import type {StyleProp, TextStyle} from 'react-native';
import Onyx from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import * as Emojis from '@assets/emojis';
import type {Emoji, HeaderEmoji, PickerEmojis} from '@assets/emojis/types';
import Text from '@components/Text';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {FrequentlyUsedEmoji, Locale} from '@src/types/onyx';
Expand All @@ -19,6 +22,10 @@ type EmojiPickerListItem = EmojiSpacer | Emoji | HeaderEmoji;
type EmojiPickerList = EmojiPickerListItem[];
type ReplacedEmoji = {text: string; emojis: Emoji[]; cursorPosition?: number};
type EmojiTrieModule = {default: typeof EmojiTrie};
type TextWithEmoji = {
text: string;
isEmoji: boolean;
};

const findEmojiByName = (name: string): Emoji => Emojis.emojiNameTable[name];

Expand Down Expand Up @@ -151,7 +158,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;
Expand All @@ -165,7 +172,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;
Expand Down Expand Up @@ -288,7 +295,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 [];
Expand Down Expand Up @@ -598,13 +605,83 @@ 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<TextStyle>): Array<React.JSX.Element | string> {
return processedTextArray.map(({text, isEmoji}, index) =>
isEmoji ? (
<Text
// eslint-disable-next-line react/no-array-index-key
key={index}
style={style}
>
{text}
</Text>
) : (
text
),
);
}

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

export {
findEmojiByName,
findEmojiByCode,
getEmojiName,
getLocalizedEmojiName,
getProcessedText,
getHeaderEmojis,
mergeEmojisWithFrequentlyUsedEmojis,
containsOnlyEmojis,
Expand All @@ -623,4 +700,5 @@ export {
hasAccountIDEmojiReacted,
getRemovedSkinToneEmoji,
getSpacersIndexes,
splitTextWithEmojis,
};
4 changes: 2 additions & 2 deletions src/libs/ValidationUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit fd3639a

Please sign in to comment.