Skip to content
Open
71 changes: 71 additions & 0 deletions app/containers/Emoji/Emoji.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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 { 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>
);
Comment on lines +58 to +68
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Consider accessibility: hard-coded font sizes don't respect user preferences.

Line 62 uses fixed fontSize values (16 and 30) for non-avatar emojis, which don't scale with the user's font size preferences. In contrast, avatar emojis (lines 53-54) use fontScaleLimited for better accessibility.

Consider applying consistent scaling:

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
<Text
style={[
{ color: colors.fontDefault },
isBigEmoji ? { fontSize: 30, lineHeight: 43 } : { fontSize: 16, lineHeight: 22 },
style,
isAvatar && avatarStyle
]}>
{emojiUnicode}
</Text>
);
return (
<Text
style={[
{ color: colors.fontDefault },
isBigEmoji ? { fontSize: 30 * fontScaleLimited, lineHeight: 43 * fontScaleLimited } : { fontSize: 16 * fontScaleLimited, lineHeight: 22 * fontScaleLimited },
style,
isAvatar && avatarStyle
]}>
{emojiUnicode}
</Text>
);
🤖 Prompt for AI Agents
In app/containers/Emoji/Emoji.tsx around lines 58 to 68, the Text component uses
hard-coded fontSize values (16 and 30) that don't respect user font-size
preferences; replace those fixed sizes with the same scaling utility used for
avatars (fontScaleLimited) so both big and normal emoji sizes scale with
accessibility settings, and adjust corresponding lineHeight to maintain visual
proportions (e.g., compute lineHeight from the scaled font size or apply a
multiplier) while keeping the rest of the style array and avatar handling
unchanged.

};

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} />;
};
24 changes: 24 additions & 0 deletions app/containers/Skeleton/Skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import SkeletonPlaceholder from 'react-native-skeleton-placeholder';
import { type DimensionValue, type StyleProp, type ViewStyle } from 'react-native';

import { useTheme } from '../../theme';

interface ISkeletonProps {
width?: DimensionValue;
height?: DimensionValue;
borderRadius?: number;
style?: StyleProp<ViewStyle>;
}

const Skeleton = ({ width, height, borderRadius, style }: ISkeletonProps): React.ReactElement => {
const { colors } = useTheme();

return (
<SkeletonPlaceholder backgroundColor={colors.surfaceTint}>
<SkeletonPlaceholder.Item width={width} height={height} borderRadius={borderRadius} style={style} />
</SkeletonPlaceholder>
);
};

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

import Skeleton from '../index';

// Mock useTheme
jest.mock('../../theme', () => ({
useTheme: () => ({ colors: { surfaceTint: 'gray', surfaceHover: 'lightgray' } })
}));

jest.mock('react-native-skeleton-placeholder', () => {
const { View } = require('react-native');
const MockSkeleton = ({ children }: any) => <View testID="skeleton-placeholder">{children}</View>;
MockSkeleton.Item = ({ children }: any) => <View testID="skeleton-item">{children}</View>;
return MockSkeleton;
});

describe('Skeleton', () => {
it('renders correctly', () => {
const { toJSON } = render(<Skeleton width={100} height={100} />);
expect(toJSON()).toMatchSnapshot();
});

it('applies styles correctly', () => {
const { toJSON } = render(<Skeleton width={50} height={50} borderRadius={10} />);
expect(toJSON()).toMatchSnapshot();
});
});
1 change: 1 addition & 0 deletions app/containers/Skeleton/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './Skeleton';
95 changes: 55 additions & 40 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 Expand Up @@ -1201,6 +1179,42 @@ exports[`Story Snapshots: Image should match snapshot 1`] = `
}
}
>
<View
onLayout={[Function]}
>
<View
style={
{
"backgroundColor": "transparent",
}
}
>
<View
style={
[
{
"borderRadius": 4,
"height": 100,
"width": 100,
},
[
{
"overflow": "hidden",
},
{
"backgroundColor": "#F7F8FA",
},
],
{
"borderRadius": 4,
"height": 100,
"width": 100,
},
]
}
/>
</View>
</View>
<ViewManagerAdapter_ExpoImage
borderColor={4291546833}
containerViewRef={"[React.ref]"}
Expand All @@ -1211,10 +1225,11 @@ exports[`Story Snapshots: Image should match snapshot 1`] = `
"top": "50%",
}
}
height={300}
height={0}
nativeViewRef={"[React.ref]"}
onError={[Function]}
onLoad={[Function]}
onLoadEnd={[Function]}
onLoadStart={[Function]}
onProgress={[Function]}
placeholder={[]}
Expand All @@ -1228,12 +1243,12 @@ exports[`Story Snapshots: Image should match snapshot 1`] = `
style={
{
"borderColor": "#CBCED1",
"height": 300,
"width": 300,
"height": 0,
"width": 0,
}
}
transition={null}
width={300}
width={0}
/>
</Text>
</Text>
Expand Down
Loading