Skip to content

Commit

Permalink
Merge pull request #30416 from VickyStash/ts-migration/emojiSuggestio…
Browse files Browse the repository at this point in the history
…ns-component

[TS migration] Migrate 'EmojiSuggestions.js' component to TypeScript
  • Loading branch information
johnmlee101 authored Nov 13, 2023
2 parents 629c6de + 05b718e commit a48e5ba
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 84 deletions.
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;
}

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)
.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

0 comments on commit a48e5ba

Please sign in to comment.