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

[TS migration] Migrate 'EmojiSuggestions.js' component to TypeScript #30416

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,85 +1,64 @@
import PropTypes from 'prop-types';
import React from 'react';
import React, {ReactElement} from 'react';
import {View} from 'react-native';
import _ from 'underscore';
import type {SimpleEmoji} from '@libs/EmojiTrie';
import * as EmojiUtils from '@libs/EmojiUtils';
import getStyledTextArray from '@libs/GetStyledTextArray';
import styles from '@styles/styles';
import * as StyleUtils from '@styles/StyleUtils';
import AutoCompleteSuggestions from './AutoCompleteSuggestions';
import Text from './Text';

const propTypes = {
type MeasureParentContainerCallback = (x: number, y: number, width: number) => void;

type EmojiSuggestionsProps = {
/** The index of the highlighted emoji */
highlightedEmojiIndex: PropTypes.number,
highlightedEmojiIndex?: number;

/** Array of suggested emoji */
emojis: PropTypes.arrayOf(
PropTypes.shape({
/** The emoji code */
code: PropTypes.string.isRequired,

/** The name of the emoji */
name: PropTypes.string.isRequired,

/** Array of different skin tone variants.
* If provided, it will be indexed with props.preferredSkinToneIndex */
types: PropTypes.arrayOf(PropTypes.string.isRequired),
}),
).isRequired,
emojis: SimpleEmoji[];

/** Fired when the user selects an emoji */
onSelect: PropTypes.func.isRequired,
onSelect: (index: number) => void;

/** Emoji prefix that follows the colon */
prefix: PropTypes.string.isRequired,
prefix: string;

/** Show that we can use large emoji picker. Depending on available space
* and whether the input is expanded, we can have a small or large emoji
* suggester. When this value is false, the suggester will have a height of
* 2.5 items. When this value is true, the height can be up to 5 items. */
isEmojiPickerLarge: PropTypes.bool.isRequired,
isEmojiPickerLarge: boolean;

/** Stores user's preferred skin tone */
preferredSkinToneIndex: PropTypes.number.isRequired,
preferredSkinToneIndex: number;

/** Meaures the parent container's position and dimensions. */
measureParentContainer: PropTypes.func,
};

const defaultProps = {
highlightedEmojiIndex: 0,
measureParentContainer: () => {},
measureParentContainer: (callback: MeasureParentContainerCallback) => void;
};

/**
* Create unique keys for each emoji item
* @param {Object} item
* @param {Number} index
* @returns {String}
*/
const keyExtractor = (item, index) => `${item.name}+${index}}`;
const keyExtractor = (item: SimpleEmoji, index: number): string => `${item.name}+${index}}`;

function EmojiSuggestions(props) {
function EmojiSuggestions({emojis, onSelect, prefix, isEmojiPickerLarge, preferredSkinToneIndex, highlightedEmojiIndex = 0, measureParentContainer = () => {}}: EmojiSuggestionsProps) {
/**
* Render an emoji suggestion menu item component.
* @param {Object} item
* @returns {JSX.Element}
*/
const renderSuggestionMenuItem = (item) => {
const styledTextArray = getStyledTextArray(item.name, props.prefix);
const renderSuggestionMenuItem = (item: SimpleEmoji): ReactElement => {
const styledTextArray = getStyledTextArray(item.name, prefix);

return (
<View style={styles.autoCompleteSuggestionContainer}>
<Text style={styles.emojiSuggestionsEmoji}>{EmojiUtils.getEmojiCodeWithSkinColor(item, props.preferredSkinToneIndex)}</Text>
<Text style={styles.emojiSuggestionsEmoji}>{EmojiUtils.getEmojiCodeWithSkinColor(item, preferredSkinToneIndex)}</Text>
<Text
numberOfLines={2}
style={styles.emojiSuggestionsText}
>
:
{_.map(styledTextArray, ({text, isColored}, i) => (
{styledTextArray.map(({text, isColored}) => (
<Text
key={`${text}+${i}`}
key={`${text}+${isColored}`}
style={StyleUtils.getColoredBackgroundStyle(isColored)}
>
{text}
Expand All @@ -93,20 +72,18 @@ function EmojiSuggestions(props) {

return (
<AutoCompleteSuggestions
suggestions={props.emojis}
suggestions={emojis}
renderSuggestionMenuItem={renderSuggestionMenuItem}
keyExtractor={keyExtractor}
highlightedSuggestionIndex={props.highlightedEmojiIndex}
onSelect={props.onSelect}
isSuggestionPickerLarge={props.isEmojiPickerLarge}
highlightedSuggestionIndex={highlightedEmojiIndex}
onSelect={onSelect}
isSuggestionPickerLarge={isEmojiPickerLarge}
accessibilityLabelExtractor={keyExtractor}
measureParentContainer={props.measureParentContainer}
measureParentContainer={measureParentContainer}
/>
);
}

EmojiSuggestions.propTypes = propTypes;
EmojiSuggestions.defaultProps = defaultProps;
EmojiSuggestions.displayName = 'EmojiSuggestions';

export default EmojiSuggestions;
77 changes: 41 additions & 36 deletions src/libs/EmojiTrie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,20 @@ import CONST from '@src/CONST';
import Timing from './actions/Timing';
import Trie from './Trie';

type Emoji = {
type HeaderEmoji = {
code: string;
header?: boolean;
icon?: React.FC<SvgProps>;
name?: string;
header: boolean;
icon: React.FC<SvgProps>;
};

type SimpleEmoji = {
code: string;
name: string;
types?: string[];
};

type Emoji = HeaderEmoji | SimpleEmoji;

type LocalizedEmoji = {
name?: string;
keywords: string[];
Expand Down Expand Up @@ -51,7 +57,7 @@ type EmojiTrie = {
* @param name The localized name of the emoji.
* @param shouldPrependKeyword Prepend the keyword (instead of append) to the suggestions
*/
function addKeywordsToTrie(trie: Trie<EmojiMetaData>, keywords: string[], item: Emoji, name: string, shouldPrependKeyword = false) {
function addKeywordsToTrie(trie: Trie<EmojiMetaData>, keywords: string[], item: SimpleEmoji, name: string, shouldPrependKeyword = false) {
keywords.forEach((keyword) => {
const keywordNode = trie.search(keyword);
if (!keywordNode) {
Expand Down Expand Up @@ -84,37 +90,35 @@ function createTrie(lang: SupportedLanguage = CONST.LOCALES.DEFAULT): Trie<Emoji
const defaultLangEmojis: LocalizedEmojis = localeEmojis[CONST.LOCALES.DEFAULT];
const isDefaultLocale = lang === CONST.LOCALES.DEFAULT;

emojis.forEach((item: Emoji) => {
if (!item.name) {
return;
}
Comment on lines -88 to -90
Copy link
Contributor

Choose a reason for hiding this comment

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

Why was this condition removed?


const englishName = item.name;
const localeName = langEmojis?.[item.code]?.name ?? englishName;

const node = trie.search(localeName);
if (!node) {
trie.add(localeName, {code: item.code, types: item.types, name: localeName, suggestions: []});
} else {
trie.update(localeName, {code: item.code, types: item.types, name: localeName, suggestions: node.metaData.suggestions});
}

const nameParts = getNameParts(localeName).slice(1); // We remove the first part because we already index the full name.
addKeywordsToTrie(trie, nameParts, item, localeName);

// Add keywords for both the locale language and English to enable users to search using either language.
const keywords = (langEmojis?.[item.code]?.keywords ?? []).concat(isDefaultLocale ? [] : defaultLangEmojis?.[item.code]?.keywords ?? []);
addKeywordsToTrie(trie, keywords, item, localeName);

/**
* If current language isn't the default, prepend the English name of the emoji in the suggestions as well.
* We do this because when the user types the english name of the emoji, we want to show the emoji in the suggestions before all the others.
*/
if (!isDefaultLocale) {
const englishNameParts = getNameParts(englishName);
addKeywordsToTrie(trie, englishNameParts, item, localeName, true);
}
});
emojis
.filter((item: Emoji): item is SimpleEmoji => !(item as HeaderEmoji).header)
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you explain the purpose of this filter()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@fabioh8010 removing of the if condition and adding of this filter are related to each other.
emojis is an array that has one header emoji

{
        header: true,
        icon: Smiley,
        code: 'smileysAndEmotion',
 }

and other usual emojis.

Header emoji is not used in emoji suggestions and was filtered out before with a check.

if (!item.name) {
    return;
}

I replaced it with a filter instead to add type narrowing to identify that only simple emojis are used in the suggestions.

Btw, before the migration of the EmojiTrie file it also checked for the header field instead of name to filter header emoji out.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for explanation @VickyStash !

.forEach((item: SimpleEmoji) => {
const englishName = item.name;
const localeName = langEmojis?.[item.code]?.name ?? englishName;

const node = trie.search(localeName);
if (!node) {
trie.add(localeName, {code: item.code, types: item.types, name: localeName, suggestions: []});
} else {
trie.update(localeName, {code: item.code, types: item.types, name: localeName, suggestions: node.metaData.suggestions});
}

const nameParts = getNameParts(localeName).slice(1); // We remove the first part because we already index the full name.
addKeywordsToTrie(trie, nameParts, item, localeName);

// Add keywords for both the locale language and English to enable users to search using either language.
const keywords = (langEmojis?.[item.code]?.keywords ?? []).concat(isDefaultLocale ? [] : defaultLangEmojis?.[item.code]?.keywords ?? []);
addKeywordsToTrie(trie, keywords, item, localeName);

/**
* If current language isn't the default, prepend the English name of the emoji in the suggestions as well.
* We do this because when the user types the english name of the emoji, we want to show the emoji in the suggestions before all the others.
*/
if (!isDefaultLocale) {
const englishNameParts = getNameParts(englishName);
addKeywordsToTrie(trie, englishNameParts, item, localeName, true);
}
});

return trie;
}
Expand All @@ -124,3 +128,4 @@ const emojiTrie: EmojiTrie = supportedLanguages.reduce((prev, cur) => ({...prev,
Timing.end(CONST.TIMING.TRIE_INITIALIZATION);

export default emojiTrie;
export type {SimpleEmoji};
2 changes: 1 addition & 1 deletion src/styles/StyleUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1018,7 +1018,7 @@ function getAutoCompleteSuggestionContainerStyle(itemsHeight: number): ViewStyle
/**
* Select the correct color for text.
*/
function getColoredBackgroundStyle(isColored: boolean): ViewStyle {
function getColoredBackgroundStyle(isColored: boolean): TextStyle {
return {backgroundColor: isColored ? themeColors.link : undefined};
}

Expand Down
Loading