Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enlarge emojis in other contexts than just single character messages #47547

Open
wants to merge 49 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
8d93e58
WIP on emojis enlarge
VickyStash Aug 5, 2024
8fdd879
Add more options for testing
VickyStash Aug 8, 2024
1a776ab
Improve messages with enlarged emojis display
VickyStash Aug 12, 2024
957737a
Code clean up
VickyStash Aug 12, 2024
18997d9
Merge branch 'refs/heads/main' into feature/large-emojis
VickyStash Aug 12, 2024
27fb058
Fix composer height when only emojis are entered
VickyStash Aug 14, 2024
3ab61f4
Increase emojis in the display name
VickyStash Aug 14, 2024
2b0caa2
Merge branch 'refs/heads/main' into feature/large-emojis
VickyStash Aug 16, 2024
e750501
Fix emojis are cut off in some places on ios
VickyStash Aug 16, 2024
3175d40
Lint fixes
VickyStash Aug 16, 2024
8b0828b
Try to fix react compiler error
VickyStash Aug 16, 2024
776aacd
Resolve TODO related to the text selection
VickyStash Aug 19, 2024
6981d7c
Merge branch 'refs/heads/main' into feature/large-emojis
VickyStash Aug 19, 2024
41ffb05
Fix cursor jumping on ios
VickyStash Aug 20, 2024
2007c58
Fix emojis are cut off in the workspace list on ios
VickyStash Aug 20, 2024
9250cd9
Fix composer height
VickyStash Aug 20, 2024
46ddde2
Merge branch 'refs/heads/main' into feature/large-emojis
VickyStash Aug 20, 2024
2dd7773
Lint fix
VickyStash Aug 20, 2024
cc8e8b1
Fix large emojis overlap
VickyStash Aug 21, 2024
435e777
Improve sender display
VickyStash Aug 21, 2024
d8a3fd2
Merge branch 'refs/heads/main' into feature/large-emojis
VickyStash Aug 23, 2024
6ef2595
Lint fix
VickyStash Aug 23, 2024
25a4ee9
Merge branch 'refs/heads/main' into feature/large-emojis
VickyStash Aug 26, 2024
7cf9b77
Fix emoji alignment
VickyStash Aug 26, 2024
3af678f
Merge branch 'refs/heads/main' into feature/large-emojis
VickyStash Aug 29, 2024
272a32d
Update display name emoji size after merging main
VickyStash Aug 29, 2024
87e41e5
Compiler fix
VickyStash Aug 29, 2024
dda7fdf
Code improvement
VickyStash Aug 30, 2024
0705fee
Merge branch 'refs/heads/main' into feature/large-emojis
VickyStash Sep 2, 2024
8ecc202
Fix web emoji display
VickyStash Sep 3, 2024
3e2f369
Merge branch 'refs/heads/main' into feature/large-emojis
VickyStash Sep 10, 2024
4f4ca61
Merge branch 'refs/heads/main' into feature/large-emojis
VickyStash Sep 11, 2024
9c30d73
Add missed import
VickyStash Sep 11, 2024
528c08f
Fix regex usage
VickyStash Sep 12, 2024
eb3a958
Fix only emojis cropping in the composer on ios
VickyStash Sep 12, 2024
1befeeb
Merge branch 'refs/heads/main' into feature/large-emojis
VickyStash Sep 13, 2024
ba756a5
Merge branch 'refs/heads/main' into feature/large-emojis
VickyStash Sep 23, 2024
4713c7e
Fix lint check errors
VickyStash Sep 23, 2024
35af453
Merge branch 'refs/heads/main' into feature/large-emojis
VickyStash Oct 1, 2024
70190a4
Minor fix
VickyStash Oct 1, 2024
743d3c2
Merge branch 'refs/heads/main' into feature/large-emojis
VickyStash Oct 4, 2024
bce816d
TS fix
VickyStash Oct 4, 2024
9a4883b
Update react-native-live-markdown version
VickyStash Oct 4, 2024
cde40ee
Revert "Update react-native-live-markdown version"
VickyStash Oct 4, 2024
8e703d2
Merge branch 'refs/heads/main' into feature/large-emojis
VickyStash Oct 13, 2024
592dc56
Add keys to get rid of lint errors
VickyStash Oct 13, 2024
8010e02
Clean up code duplicates
VickyStash Oct 13, 2024
b2fe231
Merge branch 'refs/heads/main' into feature/large-emojis
VickyStash Oct 16, 2024
0c14e87
Reapply changes after merging main
VickyStash Oct 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2345,8 +2345,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 @@ -2384,6 +2384,10 @@ const CONST = {
return new RegExp(`[\\n\\s]|${this.SPECIAL_CHAR.source}|${this.EMOJI.source}`, 'gu');
},

get ALL_EMOJIS() {
return new RegExp(CONST.REGEX.EMOJIS, CONST.REGEX.EMOJIS.flags.concat('g'));
},

MERGED_ACCOUNT_PREFIX: /^(MERGED_\d+@)/,

ROUTES: {
Expand Down
14 changes: 11 additions & 3 deletions src/components/Composer/index.native.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type {MarkdownStyle} from '@expensify/react-native-live-markdown';
import mimeDb from 'mime-db';
import type {ForwardedRef} from 'react';
import React, {useCallback, useEffect, useMemo, useRef} from 'react';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import type {NativeSyntheticEvent, TextInput, TextInputChangeEventData, TextInputPasteEventData} from 'react-native';
import {StyleSheet} from 'react-native';
import type {FileObject} from '@components/AttachmentModal';
Expand All @@ -16,6 +16,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import updateIsFullComposerAvailable from '@libs/ComposerUtils/updateIsFullComposerAvailable';
import * as EmojiUtils from '@libs/EmojiUtils';
import * as FileUtils from '@libs/fileDownload/FileUtils';
import variables from '@styles/variables';
import type {ComposerProps} from './types';

const excludeNoStyles: Array<keyof MarkdownStyle> = [];
Expand All @@ -42,6 +43,7 @@ function Composer(
ref: ForwardedRef<TextInput>,
) {
const textInput = useRef<AnimatedMarkdownTextInputRef | null>(null);
const [hasMultipleLines, setHasMultipleLines] = useState(false);
const {isFocused, shouldResetFocusRef} = useResetComposerFocus(textInput);
const textContainsOnlyEmojis = useMemo(() => EmojiUtils.containsOnlyEmojis(value ?? ''), [value]);
const theme = useTheme();
Expand Down Expand Up @@ -107,7 +109,10 @@ function Composer(
);

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, textContainsOnlyEmojis && hasMultipleLines ? styles.onlyEmojisTextLineHeight : {}]),
[style, textContainsOnlyEmojis, hasMultipleLines, styles],
);

return (
<RNMarkdownTextInput
Expand All @@ -116,7 +121,10 @@ function Composer(
placeholderTextColor={theme.placeholderText}
ref={setTextInputRef}
value={value}
onContentSizeChange={(e) => updateIsFullComposerAvailable({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles, true)}
onContentSizeChange={(e) => {
setHasMultipleLines(e.nativeEvent.contentSize.height > variables.componentSizeLarge);
updateIsFullComposerAvailable({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles, true);
}}
rejectResponderTermination={false}
smartInsertDelete={false}
textAlignVertical="center"
Expand Down
7 changes: 5 additions & 2 deletions src/components/Composer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import * as FileUtils from '@libs/fileDownload/FileUtils';
import focusComposerWithDelay from '@libs/focusComposerWithDelay';
import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition';
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import type {ComposerProps} from './types';

Expand Down Expand Up @@ -102,6 +103,7 @@ function Composer(
const [caretContent, setCaretContent] = useState('');
const [valueBeforeCaret, setValueBeforeCaret] = useState('');
const [textInputWidth, setTextInputWidth] = useState<ViewStyle['width']>('');
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 @@ -380,10 +382,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 @@ -403,6 +405,7 @@ function Composer(
{...props}
onSelectionChange={addCursorPositionToSelectionChange}
onContentSizeChange={(e) => {
setHasMultipleLines(e.nativeEvent.contentSize.height > variables.componentSizeLarge);
setTextInputWidth(`${e.nativeEvent.contentSize.width}px`);
updateIsFullComposerAvailable({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles);
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import React from 'react';
import React, {useMemo} from 'react';
import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-html';
import EmojiWithTooltip from '@components/EmojiWithTooltip';
import useThemeStyles from '@hooks/useThemeStyles';

function EmojiRenderer({tnode}: CustomRendererProps<TText | TPhrasing>) {
const styles = useThemeStyles();
const style = 'islarge' in tnode.attributes ? styles.onlyEmojisText : {};
const style = useMemo(() => {
if ('islarge' in tnode.attributes) {
return styles.onlyEmojisText;
}

if ('ismedium' in tnode.attributes) {
return [styles.emojisWithTextFontSize, styles.verticalAlignTopText];
}

return null;
}, [tnode.attributes, styles]);
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 @@ -37,7 +37,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);
}

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 @@ -30,7 +30,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
3 changes: 2 additions & 1 deletion src/components/TextInput/BaseTextInput/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,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],
);
Expand Down
9 changes: 8 additions & 1 deletion src/components/TextWithTooltip/index.native.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
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
? processedTextArray.map(({text: textItem, isEmoji}) => (isEmoji ? <Text style={[style, styles.emojisFontFamily]}>{textItem}</Text> : textItem))
: text}
</Text>
);
}
Expand Down
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
2 changes: 1 addition & 1 deletion src/libs/ComposerUtils/updateIsFullComposerAvailable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function updateIsFullComposerAvailable(props: ComposerProps, event: NativeSynthe
return;
}
const totalHeight = inputHeight + paddingTopAndBottom;
const isFullComposerAvailable = totalHeight >= CONST.COMPOSER.FULL_COMPOSER_MIN_HEIGHT;
const isFullComposerAvailable = totalHeight > CONST.COMPOSER.FULL_COMPOSER_MIN_HEIGHT;
if (isFullComposerAvailable !== props.isFullComposerAvailable) {
props.setIsFullComposerAvailable?.(isFullComposerAvailable);
}
Expand Down
64 changes: 61 additions & 3 deletions src/libs/EmojiUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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 @@ -148,7 +152,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 @@ -162,7 +166,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 @@ -285,7 +289,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 @@ -586,6 +590,59 @@ 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 = CONST.REGEX.ALL_EMOJIS.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 = CONST.REGEX.ALL_EMOJIS;

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

export {
Expand All @@ -611,4 +668,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 @@ -41,7 +41,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 @@ -336,7 +336,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
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 @@ -160,18 +160,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
Loading
Loading