Skip to content
73 changes: 73 additions & 0 deletions app/containers/Emoji/Emoji.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React from 'react';
import { Text, useWindowDimensions } from 'react-native';
import type { StyleProp, TextStyle, ImageStyle } from 'react-native';

import { useTheme } from '../../theme';
import useShortnameToUnicode from '../../lib/hooks/useShortnameToUnicode';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
import { useAppSelector } from '../../lib/hooks/useAppSelector';
import { getUserSelector } from '../../selectors/login';
import { useResponsiveLayout } from '../../lib/hooks/useResponsiveLayout/useResponsiveLayout';

interface ISharedEmojiProps {
literal?: string;
isBigEmoji?: boolean;
style?: StyleProp<TextStyle>;
isAvatar?: boolean;
getCustomEmoji?: (name: string) => any;
customEmoji?: any;
}

const Emoji = ({ literal, isBigEmoji, style, isAvatar, getCustomEmoji, customEmoji }: ISharedEmojiProps) => {
const { colors } = useTheme();
const { formatShortnameToUnicode } = useShortnameToUnicode();
const { fontScale } = useWindowDimensions();
const { fontScaleLimited } = useResponsiveLayout();


// Calculate emoji sizes once to avoid duplication
const customEmojiSize = {
width: 15 * fontScale,
height: 15 * fontScale
};
const customEmojiBigSize = {
width: 30 * fontScale,
height: 30 * fontScale
};

if (customEmoji) {
return <CustomEmoji style={[isBigEmoji ? customEmojiBigSize : customEmojiSize, style as StyleProp<ImageStyle>]} emoji={customEmoji} />;
}

if (!literal) {
return null;
}

const emojiUnicode = formatShortnameToUnicode(literal);
const emojiName = literal.replace(/:/g, '');
const foundCustomEmoji = getCustomEmoji?.(emojiName);

if (foundCustomEmoji) {
return <CustomEmoji style={[isBigEmoji ? customEmojiBigSize : customEmojiSize, style as StyleProp<ImageStyle>]} emoji={foundCustomEmoji} />;
}

const avatarStyle = {
fontSize: 30 * fontScaleLimited,
lineHeight: 30 * fontScaleLimited,
textAlign: 'center' as const
};

return (
<Text
style={[
{ color: colors.fontDefault },
isBigEmoji ? { fontSize: 30, lineHeight: 43 } : { fontSize: 16, lineHeight: 22 },
style,
isAvatar && avatarStyle
]}>
{emojiUnicode}
</Text>
);
};

export default Emoji;
38 changes: 38 additions & 0 deletions app/containers/Emoji/__tests__/Emoji.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import { render } from '@testing-library/react-native';

import Emoji from '../Emoji';

// Mock dependencies
jest.mock('../../../lib/hooks/useShortnameToUnicode', () => ({
__esModule: true,
default: () => ({
formatShortnameToUnicode: (str: string) => (str === ':smile:' ? '😄' : str)
})
}));

jest.mock('../../EmojiPicker/CustomEmoji', () => {
const { View } = require('react-native');
return (props: any) => <View testID="mock-custom-emoji" {...props} />;
});

jest.mock('../../../theme', () => ({
useTheme: () => ({ colors: { fontDefault: 'black' } })
}));

jest.mock('../../../lib/hooks/useResponsiveLayout/useResponsiveLayout', () => ({
useResponsiveLayout: () => ({ fontScaleLimited: 1 })
}));

describe('Emoji', () => {
it('renders standard emoji correctly', () => {
const { getByText } = render(<Emoji literal=":smile:" />);
expect(getByText('😄')).toBeTruthy();
});

it('renders custom emoji correctly', () => {
const customEmoji = { name: 'party_parrot', extension: 'gif' };
const { getByTestId } = render(<Emoji customEmoji={customEmoji} />);
expect(getByTestId('mock-custom-emoji')).toBeTruthy();
});
});
1 change: 1 addition & 0 deletions app/containers/Emoji/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Emoji } from './Emoji';
11 changes: 3 additions & 8 deletions app/containers/EmojiPicker/Emoji.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import React from 'react';
import { Text } from 'react-native';

import useShortnameToUnicode from '../../lib/hooks/useShortnameToUnicode';
import styles from './styles';
import CustomEmoji from './CustomEmoji';
import { type IEmojiProps } from './interfaces';
import SharedEmoji from '../Emoji/Emoji';

export const Emoji = ({ emoji }: IEmojiProps): React.ReactElement => {
const { formatShortnameToUnicode } = useShortnameToUnicode(true);
const unicodeEmoji = formatShortnameToUnicode(`:${emoji}:`);

if (typeof emoji === 'string') {
return <Text style={styles.categoryEmoji}>{unicodeEmoji}</Text>;
return <SharedEmoji literal={`:${emoji}:`} style={styles.categoryEmoji} />;
}
return <CustomEmoji style={styles.customCategoryEmoji} emoji={emoji} />;
return <SharedEmoji customEmoji={emoji} style={styles.customCategoryEmoji} />;
};
50 changes: 14 additions & 36 deletions app/containers/markdown/__snapshots__/Markdown.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -488,13 +488,11 @@ exports[`Story Snapshots: Emoji should match snapshot 1`] = `
"color": "#2F343D",
},
{
"backgroundColor": "transparent",
"fontFamily": "Inter",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 22,
"textAlign": "left",
},
undefined,
undefined,
]
}
>
Expand All @@ -507,13 +505,11 @@ exports[`Story Snapshots: Emoji should match snapshot 1`] = `
"color": "#2F343D",
},
{
"backgroundColor": "transparent",
"fontFamily": "Inter",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 22,
"textAlign": "left",
},
undefined,
undefined,
]
}
>
Expand All @@ -526,13 +522,11 @@ exports[`Story Snapshots: Emoji should match snapshot 1`] = `
"color": "#2F343D",
},
{
"backgroundColor": "transparent",
"fontFamily": "Inter",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 22,
"textAlign": "left",
},
undefined,
undefined,
]
}
>
Expand Down Expand Up @@ -600,19 +594,14 @@ exports[`Story Snapshots: Emoji should match snapshot 1`] = `
"color": "#2F343D",
},
{
"backgroundColor": "transparent",
"fontFamily": "Inter",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 22,
"textAlign": "left",
},
{},
false,
undefined,
undefined,
]
}
>

😂
</Text>
<Text
Expand All @@ -639,19 +628,14 @@ exports[`Story Snapshots: Emoji should match snapshot 1`] = `
"color": "#2F343D",
},
{
"backgroundColor": "transparent",
"fontFamily": "Inter",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 22,
"textAlign": "left",
},
{},
false,
undefined,
undefined,
]
}
>

👍
</Text>
</Text>
Expand Down Expand Up @@ -863,13 +847,11 @@ exports[`Story Snapshots: Emoji should match snapshot 1`] = `
"color": "#2F343D",
},
{
"backgroundColor": "transparent",
"fontFamily": "Inter",
"fontSize": 30,
"fontWeight": "400",
"lineHeight": 43,
"textAlign": "left",
},
undefined,
undefined,
]
}
>
Expand All @@ -882,15 +864,11 @@ exports[`Story Snapshots: Emoji should match snapshot 1`] = `
"color": "#2F343D",
},
{
"backgroundColor": "transparent",
"fontFamily": "Inter",
"fontSize": 30,
"fontWeight": "400",
"lineHeight": 43,
"textAlign": "left",
},
{},
false,
undefined,
undefined,
]
}
>
Expand Down
68 changes: 6 additions & 62 deletions app/containers/markdown/components/emoji/Emoji.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
import React, { useContext } from 'react';
import { Text, useWindowDimensions } from 'react-native';
import { type Emoji as EmojiProps } from '@rocket.chat/message-parser';

import Plain from '../Plain';
import useShortnameToUnicode from '../../../../lib/hooks/useShortnameToUnicode';
import { useTheme } from '../../../../theme';
import styles from '../../styles';
import CustomEmoji from '../../../EmojiPicker/CustomEmoji';
import MarkdownContext from '../../contexts/MarkdownContext';
import { useAppSelector } from '../../../../lib/hooks/useAppSelector';
import { getUserSelector } from '../../../../selectors/login';
import { useResponsiveLayout } from '../../../../lib/hooks/useResponsiveLayout/useResponsiveLayout';
import SharedEmoji from '../../../Emoji/Emoji';

interface IEmojiProps {
block: EmojiProps;
Expand All @@ -20,69 +12,21 @@ interface IEmojiProps {
isAvatar?: boolean;
}

function getEmojiToken(block: EmojiProps, isAvatar: boolean) {
const getEmojiToken = (block: EmojiProps, isAvatar: boolean) => {
if ('unicode' in block) {
return block.unicode;
}

if (isAvatar) {
return block.value?.value;
}

return block?.shortCode ? `:${block.shortCode}:` : `:${block.value?.value}:`;
}
};

const Emoji = ({ block, isBigEmoji, style = {}, index, isAvatar = false }: IEmojiProps) => {
const { colors } = useTheme();
const Emoji = ({ block, isBigEmoji, style, index, isAvatar }: IEmojiProps) => {
const { getCustomEmoji } = useContext(MarkdownContext);
const { fontScale } = useWindowDimensions();
const { fontScaleLimited } = useResponsiveLayout();
const { formatShortnameToUnicode } = useShortnameToUnicode();
const spaceLeft = index && index > 0 ? ' ' : '';
const convertAsciiEmoji = useAppSelector(state => getUserSelector(state)?.settings?.preferences?.convertAsciiEmoji);

if ('unicode' in block) {
return <Text style={[{ color: colors.fontDefault }, isBigEmoji ? styles.textBig : styles.text]}>{block.unicode}</Text>;
}

const emojiToken = getEmojiToken(block, isAvatar);
const emojiUnicode = formatShortnameToUnicode(emojiToken);
const emoji = getCustomEmoji?.(block.value?.value.replace(/\:/g, ''));
const isAsciiEmoji = !!block?.shortCode && block.value?.value !== block?.shortCode;
const displayAsciiEmoji = !convertAsciiEmoji && isAsciiEmoji && !!block.value;
const customEmojiSize = {
width: 15 * fontScale,
height: 15 * fontScale
};

const customEmojiBigSize = {
width: 30 * fontScale,
height: 30 * fontScale
};

const avatarStyle = {
fontSize: 30 * fontScaleLimited,
lineHeight: 30 * fontScaleLimited,
textAlign: 'center',
textAlignVertical: 'center'
};

if (emoji) {
return <CustomEmoji style={[isBigEmoji ? customEmojiBigSize : customEmojiSize, style]} emoji={emoji} />;
}
const literal = getEmojiToken(block, !!isAvatar);

return (
<Text
style={[
{ color: colors.fontDefault },
isBigEmoji && emojiToken !== emojiUnicode ? styles.textBig : styles.text,
style,
isAvatar && avatarStyle
]}>
{spaceLeft}
{displayAsciiEmoji ? <Plain value={block.value!.value} /> : emojiUnicode}
</Text>
);
return <SharedEmoji literal={literal} isBigEmoji={isBigEmoji} style={style} isAvatar={isAvatar} getCustomEmoji={getCustomEmoji} />;
};

export default Emoji;