diff --git a/assets/emojis/index.ts b/assets/emojis/index.ts index 5e5565da1499..02328001674e 100644 --- a/assets/emojis/index.ts +++ b/assets/emojis/index.ts @@ -1,10 +1,13 @@ +import type {Locale} from '@src/types/onyx'; import emojis from './common'; import enEmojis from './en'; import esEmojis from './es'; -import type {Emoji} from './types'; +import type {Emoji, EmojisList} from './types'; type EmojiTable = Record; +type LocaleEmojis = Partial>; + const emojiNameTable = emojis.reduce((prev, cur) => { const newValue = prev; if (!('header' in cur) && cur.name) { @@ -26,10 +29,10 @@ const emojiCodeTableWithSkinTones = emojis.reduce((prev, cur) => { return newValue; }, {}); -const localeEmojis = { +const localeEmojis: LocaleEmojis = { en: enEmojis, es: esEmojis, -} as const; +}; export default emojis; export {emojiNameTable, emojiCodeTableWithSkinTones, localeEmojis}; diff --git a/assets/emojis/types.ts b/assets/emojis/types.ts index e8c222fde948..e659924a7fa4 100644 --- a/assets/emojis/types.ts +++ b/assets/emojis/types.ts @@ -3,7 +3,7 @@ import type IconAsset from '@src/types/utils/IconAsset'; type Emoji = { code: string; name: string; - types?: string[]; + types?: readonly string[]; }; type HeaderEmoji = { @@ -12,8 +12,10 @@ type HeaderEmoji = { code: string; }; -type PickerEmojis = Array; +type PickerEmoji = Emoji | HeaderEmoji; + +type PickerEmojis = PickerEmoji[]; type EmojisList = Record; -export type {Emoji, HeaderEmoji, EmojisList, PickerEmojis}; +export type {Emoji, HeaderEmoji, EmojisList, PickerEmojis, PickerEmoji}; diff --git a/src/components/EmojiSuggestions.tsx b/src/components/EmojiSuggestions.tsx index 5904b1521f98..1c0306741048 100644 --- a/src/components/EmojiSuggestions.tsx +++ b/src/components/EmojiSuggestions.tsx @@ -1,9 +1,9 @@ import type {ReactElement} from 'react'; import React, {useCallback} from 'react'; import {View} from 'react-native'; +import type {Emoji} from '@assets/emojis/types'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {SimpleEmoji} from '@libs/EmojiTrie'; import * as EmojiUtils from '@libs/EmojiUtils'; import getStyledTextArray from '@libs/GetStyledTextArray'; import AutoCompleteSuggestions from './AutoCompleteSuggestions'; @@ -16,7 +16,7 @@ type EmojiSuggestionsProps = { highlightedEmojiIndex?: number; /** Array of suggested emoji */ - emojis: SimpleEmoji[]; + emojis: Emoji[]; /** Fired when the user selects an emoji */ onSelect: (index: number) => void; @@ -40,7 +40,7 @@ type EmojiSuggestionsProps = { /** * Create unique keys for each emoji item */ -const keyExtractor = (item: SimpleEmoji, index: number): string => `${item.name}+${index}}`; +const keyExtractor = (item: Emoji, index: number): string => `${item.name}+${index}}`; function EmojiSuggestions({emojis, onSelect, prefix, isEmojiPickerLarge, preferredSkinToneIndex, highlightedEmojiIndex = 0, measureParentContainer = () => {}}: EmojiSuggestionsProps) { const styles = useThemeStyles(); @@ -49,7 +49,7 @@ function EmojiSuggestions({emojis, onSelect, prefix, isEmojiPickerLarge, preferr * Render an emoji suggestion menu item component. */ const renderSuggestionMenuItem = useCallback( - (item: SimpleEmoji): ReactElement => { + (item: Emoji): ReactElement => { const styledTextArray = getStyledTextArray(item.name, prefix); return ( diff --git a/src/components/Reactions/AddReactionBubble.js b/src/components/Reactions/AddReactionBubble.tsx similarity index 62% rename from src/components/Reactions/AddReactionBubble.js rename to src/components/Reactions/AddReactionBubble.tsx index a9bfdd367615..52751368a0ae 100644 --- a/src/components/Reactions/AddReactionBubble.js +++ b/src/components/Reactions/AddReactionBubble.tsx @@ -1,81 +1,75 @@ -import PropTypes from 'prop-types'; import React, {useEffect, useRef} from 'react'; import {View} from 'react-native'; +import type {Emoji} from '@assets/emojis/types'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip/PopoverAnchorTooltip'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import getButtonState from '@libs/getButtonState'; import variables from '@styles/variables'; import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; +import type {AnchorOrigin} from '@userActions/EmojiPickerAction'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; +import type {ReportAction} from '@src/types/onyx'; +import type {CloseContextMenuCallback, OpenPickerCallback, PickerRefElement} from './QuickEmojiReactions/types'; -const propTypes = { +type AddReactionBubbleProps = { /** Whether it is for context menu so we can modify its style */ - isContextMenu: PropTypes.bool, + isContextMenu?: boolean; /** * Called when the user presses on the icon button. * Will have a function as parameter which you can call * to open the picker. */ - onPressOpenPicker: PropTypes.func, + onPressOpenPicker?: (openPicker: OpenPickerCallback) => void; /** * Will get called the moment before the picker opens. */ - onWillShowPicker: PropTypes.func, + onWillShowPicker?: (callback: CloseContextMenuCallback) => void; /** * Called when the user selects an emoji. */ - onSelectEmoji: PropTypes.func.isRequired, + onSelectEmoji: (emoji: Emoji) => void; /** * ReportAction for EmojiPicker. */ - reportAction: PropTypes.shape({ - reportActionID: PropTypes.string.isRequired, - }), - - ...withLocalizePropTypes, -}; - -const defaultProps = { - isContextMenu: false, - onWillShowPicker: () => {}, - onPressOpenPicker: undefined, - reportAction: {}, + reportAction: ReportAction; }; -function AddReactionBubble(props) { +function AddReactionBubble({onSelectEmoji, reportAction, onPressOpenPicker, onWillShowPicker = () => {}, isContextMenu = false}: AddReactionBubbleProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const ref = useRef(); + const ref = useRef(null); + const {translate} = useLocalize(); + useEffect(() => EmojiPickerAction.resetEmojiPopoverAnchor, []); const onPress = () => { - const openPicker = (refParam, anchorOrigin) => { + const openPicker = (refParam?: PickerRefElement, anchorOrigin?: AnchorOrigin) => { EmojiPickerAction.showEmojiPicker( () => {}, (emojiCode, emojiObject) => { - props.onSelectEmoji(emojiObject); + onSelectEmoji(emojiObject); }, - refParam || ref, + refParam ?? ref, anchorOrigin, - props.onWillShowPicker, - props.reportAction.reportActionID, + onWillShowPicker, + reportAction.reportActionID, ); }; - if (!EmojiPickerAction.emojiPickerRef.current.isEmojiPickerVisible) { - if (props.onPressOpenPicker) { - props.onPressOpenPicker(openPicker); + if (!EmojiPickerAction.emojiPickerRef.current?.isEmojiPickerVisible) { + if (onPressOpenPicker) { + onPressOpenPicker(openPicker); } else { openPicker(); } @@ -85,21 +79,21 @@ function AddReactionBubble(props) { }; return ( - + [styles.emojiReactionBubble, styles.userSelectNone, StyleUtils.getEmojiReactionBubbleStyle(hovered || pressed, false, props.isContextMenu)]} + style={({hovered, pressed}) => [styles.emojiReactionBubble, styles.userSelectNone, StyleUtils.getEmojiReactionBubbleStyle(hovered || pressed, false, isContextMenu)]} onPress={Session.checkIfActionIsAllowed(onPress)} - onMouseDown={(e) => { + onMouseDown={(event) => { // Allow text input blur when Add reaction is right clicked - if (!e || e.button === 2) { + if (!event || event.button === 2) { return; } // Prevent text input blur when Add reaction is left clicked - e.preventDefault(); + event.preventDefault(); }} - accessibilityLabel={props.translate('emojiReactions.addReactionTooltip')} + accessibilityLabel={translate('emojiReactions.addReactionTooltip')} role={CONST.ROLE.BUTTON} // disable dimming pressDimmingValue={1} @@ -110,12 +104,12 @@ function AddReactionBubble(props) { {/* This (invisible) text will make the view have the same size as a regular emoji reaction. We make the text invisible and put the icon on top of it. */} - {'\u2800\u2800'} + {'\u2800\u2800'} @@ -126,8 +120,6 @@ function AddReactionBubble(props) { ); } -AddReactionBubble.propTypes = propTypes; -AddReactionBubble.defaultProps = defaultProps; AddReactionBubble.displayName = 'AddReactionBubble'; -export default withLocalize(AddReactionBubble); +export default AddReactionBubble; diff --git a/src/components/Reactions/EmojiReactionBubble.js b/src/components/Reactions/EmojiReactionBubble.js deleted file mode 100644 index 3fd22a758f67..000000000000 --- a/src/components/Reactions/EmojiReactionBubble.js +++ /dev/null @@ -1,110 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; -import Text from '@components/Text'; -import {withCurrentUserPersonalDetailsDefaultProps} from '@components/withCurrentUserPersonalDetails'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; -import CONST from '@src/CONST'; - -const propTypes = { - /** - * The emoji codes to display in the bubble. - */ - emojiCodes: PropTypes.arrayOf(PropTypes.string).isRequired, - - /** - * Called when the user presses on the reaction bubble. - */ - onPress: PropTypes.func.isRequired, - - /** - * Called when the user long presses or right clicks - * on the reaction bubble. - */ - onReactionListOpen: PropTypes.func, - - /** - * The number of reactions to display in the bubble. - */ - count: PropTypes.number, - - /** Whether it is for context menu so we can modify its style */ - isContextMenu: PropTypes.bool, - - /** - * Returns true if the current account has reacted to the report action (with the given skin tone). - */ - hasUserReacted: PropTypes.bool, - - /** We disable reacting with emojis on report actions that have errors */ - shouldBlockReactions: PropTypes.bool, - - ...windowDimensionsPropTypes, -}; - -const defaultProps = { - count: 0, - onReactionListOpen: () => {}, - isContextMenu: false, - shouldBlockReactions: false, - - ...withCurrentUserPersonalDetailsDefaultProps, -}; - -function EmojiReactionBubble(props) { - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - return ( - [ - styles.emojiReactionBubble, - StyleUtils.getEmojiReactionBubbleStyle(hovered || pressed, props.hasUserReacted, props.isContextMenu), - props.shouldBlockReactions && styles.cursorDisabled, - styles.userSelectNone, - ]} - onPress={() => { - if (props.shouldBlockReactions) { - return; - } - - props.onPress(); - }} - onSecondaryInteraction={props.onReactionListOpen} - ref={props.forwardedRef} - enableLongPressWithHover={props.isSmallScreenWidth} - onMouseDown={(e) => { - // Allow text input blur when emoji reaction is right clicked - if (e && e.button === 2) { - return; - } - - // Prevent text input blur when emoji reaction is left clicked - e.preventDefault(); - }} - role={CONST.ROLE.BUTTON} - accessibilityLabel={props.emojiCodes.join('')} - dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} - > - {props.emojiCodes.join('')} - {props.count > 0 && {props.count}} - - ); -} - -EmojiReactionBubble.propTypes = propTypes; -EmojiReactionBubble.defaultProps = defaultProps; -EmojiReactionBubble.displayName = 'EmojiReactionBubble'; - -const EmojiReactionBubbleWithRef = React.forwardRef((props, ref) => ( - -)); - -EmojiReactionBubbleWithRef.displayName = 'EmojiReactionBubbleWithRef'; - -export default withWindowDimensions(EmojiReactionBubbleWithRef); diff --git a/src/components/Reactions/EmojiReactionBubble.tsx b/src/components/Reactions/EmojiReactionBubble.tsx new file mode 100644 index 000000000000..d83689de2dc1 --- /dev/null +++ b/src/components/Reactions/EmojiReactionBubble.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import type {PressableRef} from '@components/Pressable/GenericPressable/types'; +import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; +import Text from '@components/Text'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import type {ReactionListEvent} from '@pages/home/ReportScreenContext'; +import CONST from '@src/CONST'; + +type EmojiReactionBubbleProps = { + /** + * The emoji codes to display in the bubble. + */ + emojiCodes: string[]; + + /** + * Called when the user presses on the reaction bubble. + */ + onPress: () => void; + + /** + * Called when the user long presses or right clicks + * on the reaction bubble. + */ + onReactionListOpen?: (event: ReactionListEvent) => void; + + /** + * The number of reactions to display in the bubble. + */ + count?: number; + + /** Whether it is for context menu so we can modify its style */ + isContextMenu?: boolean; + + /** + * Returns true if the current account has reacted to the report action (with the given skin tone). + */ + hasUserReacted?: boolean; + + /** We disable reacting with emojis on report actions that have errors */ + shouldBlockReactions?: boolean; +}; + +function EmojiReactionBubble( + {onPress, onReactionListOpen = () => {}, emojiCodes, hasUserReacted = false, count = 0, isContextMenu = false, shouldBlockReactions = false}: EmojiReactionBubbleProps, + ref: PressableRef, +) { + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {isSmallScreenWidth} = useWindowDimensions(); + + return ( + [ + styles.emojiReactionBubble, + StyleUtils.getEmojiReactionBubbleStyle(hovered || pressed, hasUserReacted, isContextMenu), + shouldBlockReactions && styles.cursorDisabled, + styles.userSelectNone, + ]} + onPress={() => { + if (shouldBlockReactions) { + return; + } + + onPress(); + }} + onSecondaryInteraction={onReactionListOpen} + ref={ref} + enableLongPressWithHover={isSmallScreenWidth} + onMouseDown={(event) => { + // Allow text input blur when emoji reaction is right clicked + if (event?.button === 2) { + return; + } + + // Prevent text input blur when emoji reaction is left clicked + event.preventDefault(); + }} + role={CONST.ROLE.BUTTON} + accessibilityLabel={emojiCodes.join('')} + accessible + dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} + > + {emojiCodes.join('')} + {count > 0 && {count}} + + ); +} + +EmojiReactionBubble.displayName = 'EmojiReactionBubble'; + +export default React.forwardRef(EmojiReactionBubble); diff --git a/src/components/Reactions/MiniQuickEmojiReactions.js b/src/components/Reactions/MiniQuickEmojiReactions.tsx similarity index 56% rename from src/components/Reactions/MiniQuickEmojiReactions.js rename to src/components/Reactions/MiniQuickEmojiReactions.tsx index 376afcb9ade5..9f38da6bdb3d 100644 --- a/src/components/Reactions/MiniQuickEmojiReactions.js +++ b/src/components/Reactions/MiniQuickEmojiReactions.tsx @@ -1,49 +1,38 @@ -import PropTypes from 'prop-types'; import React, {useRef} from 'react'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {Emoji} from '@assets/emojis/types'; import BaseMiniContextMenuItem from '@components/BaseMiniContextMenuItem'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import Text from '@components/Text'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import * as EmojiUtils from '@libs/EmojiUtils'; import getButtonState from '@libs/getButtonState'; import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {baseQuickEmojiReactionsDefaultProps, baseQuickEmojiReactionsPropTypes} from './QuickEmojiReactions/BaseQuickEmojiReactions'; +import type {ReportActionReactions} from '@src/types/onyx'; +import type {BaseQuickEmojiReactionsProps} from './QuickEmojiReactions/types'; -const propTypes = { - ...baseQuickEmojiReactionsPropTypes, +type MiniQuickEmojiReactionsOnyxProps = { + /** All the emoji reactions for the report action. */ + emojiReactions: OnyxEntry; + /** The user's preferred skin tone. */ + preferredSkinTone: OnyxEntry; +}; + +type MiniQuickEmojiReactionsProps = BaseQuickEmojiReactionsProps & { /** * Will be called when the user closed the emoji picker * without selecting an emoji. */ - onEmojiPickerClosed: PropTypes.func, - - /** - * ReportAction for EmojiPicker. - */ - reportAction: PropTypes.shape({ - reportActionID: PropTypes.string.isRequired, - }), - - ...withLocalizePropTypes, - preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), -}; - -const defaultProps = { - ...baseQuickEmojiReactionsDefaultProps, - onEmojiPickerClosed: () => {}, - preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, - reportAction: {}, + onEmojiPickerClosed?: () => void; }; /** @@ -51,56 +40,63 @@ const defaultProps = { * emoji picker icon button. This is used for the mini * context menu which we just show on web, when hovering * a message. - * @param {Props} props - * @returns {JSX.Element} */ -function MiniQuickEmojiReactions(props) { +function MiniQuickEmojiReactions({ + reportAction, + onEmojiSelected, + preferredLocale = CONST.LOCALES.DEFAULT, + preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, + emojiReactions = {}, + onPressOpenPicker = () => {}, + onEmojiPickerClosed = () => {}, +}: MiniQuickEmojiReactionsProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const ref = useRef(); + const ref = useRef(null); + const {translate} = useLocalize(); const openEmojiPicker = () => { - props.onPressOpenPicker(); + onPressOpenPicker(); EmojiPickerAction.showEmojiPicker( - props.onEmojiPickerClosed, + onEmojiPickerClosed, (emojiCode, emojiObject) => { - props.onEmojiSelected(emojiObject, props.emojiReactions); + onEmojiSelected(emojiObject, emojiReactions); }, ref, undefined, () => {}, - props.reportAction.reportActionID, + reportAction.reportActionID, ); }; return ( - {_.map(CONST.QUICK_REACTIONS, (emoji) => ( + {CONST.QUICK_REACTIONS.map((emoji: Emoji) => ( props.onEmojiSelected(emoji, props.emojiReactions))} + tooltipText={`:${EmojiUtils.getLocalizedEmojiName(emoji.name, preferredLocale)}:`} + onPress={Session.checkIfActionIsAllowed(() => onEmojiSelected(emoji, emojiReactions))} > - {EmojiUtils.getPreferredEmojiCode(emoji, props.preferredSkinTone)} + {EmojiUtils.getPreferredEmojiCode(emoji, preferredSkinTone)} ))} { - if (!EmojiPickerAction.emojiPickerRef.current.isEmojiPickerVisible) { + if (!EmojiPickerAction.emojiPickerRef.current?.isEmojiPickerVisible) { openEmojiPicker(); } else { - EmojiPickerAction.emojiPickerRef.current.hideEmojiPicker(); + EmojiPickerAction.emojiPickerRef.current?.hideEmojiPicker(); } })} isDelayButtonStateComplete={false} - tooltipText={props.translate('emojiReactions.addReactionTooltip')} + tooltipText={translate('emojiReactions.addReactionTooltip')} > {({hovered, pressed}) => ( `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`, - }, - }), -)(MiniQuickEmojiReactions); + +export default withOnyx({ + preferredSkinTone: { + key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, + }, + emojiReactions: { + key: ({reportActionID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`, + }, +})(MiniQuickEmojiReactions); diff --git a/src/components/Reactions/QuickEmojiReactions/BaseQuickEmojiReactions.js b/src/components/Reactions/QuickEmojiReactions/BaseQuickEmojiReactions.js deleted file mode 100644 index c932632f7bff..000000000000 --- a/src/components/Reactions/QuickEmojiReactions/BaseQuickEmojiReactions.js +++ /dev/null @@ -1,106 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import AddReactionBubble from '@components/Reactions/AddReactionBubble'; -import EmojiReactionBubble from '@components/Reactions/EmojiReactionBubble'; -import EmojiReactionsPropTypes from '@components/Reactions/EmojiReactionsPropTypes'; -import Tooltip from '@components/Tooltip'; -import useThemeStyles from '@hooks/useThemeStyles'; -import * as EmojiUtils from '@libs/EmojiUtils'; -import * as Session from '@userActions/Session'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; - -const baseQuickEmojiReactionsPropTypes = { - emojiReactions: EmojiReactionsPropTypes, - - /** - * Callback to fire when an emoji is selected. - */ - onEmojiSelected: PropTypes.func.isRequired, - - /** - * Will be called when the emoji picker is about to show. - */ - onWillShowPicker: PropTypes.func, - - /** - * Callback to fire when the "open emoji picker" button is pressed. - * The function receives an argument which can be called - * to actually open the emoji picker. - */ - onPressOpenPicker: PropTypes.func, - - /** - * ReportAction for EmojiPicker. - */ - reportAction: PropTypes.object, - - preferredLocale: PropTypes.string, -}; - -const baseQuickEmojiReactionsDefaultProps = { - emojiReactions: {}, - onWillShowPicker: () => {}, - onPressOpenPicker: () => {}, - reportAction: {}, - preferredLocale: CONST.LOCALES.DEFAULT, -}; - -const propTypes = { - ...baseQuickEmojiReactionsPropTypes, - preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), -}; - -const defaultProps = { - ...baseQuickEmojiReactionsDefaultProps, - preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, -}; - -function BaseQuickEmojiReactions(props) { - const styles = useThemeStyles(); - return ( - - {_.map(CONST.QUICK_REACTIONS, (emoji) => ( - - - props.onEmojiSelected(emoji, props.emojiReactions))} - /> - - - ))} - props.onEmojiSelected(emoji, props.emojiReactions)} - reportAction={props.reportAction} - /> - - ); -} - -BaseQuickEmojiReactions.displayName = 'BaseQuickEmojiReactions'; -BaseQuickEmojiReactions.propTypes = propTypes; -BaseQuickEmojiReactions.defaultProps = defaultProps; -export default withOnyx({ - preferredSkinTone: { - key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, - }, - emojiReactions: { - key: ({reportActionID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`, - }, - preferredLocale: { - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - }, -})(BaseQuickEmojiReactions); - -export {baseQuickEmojiReactionsPropTypes, baseQuickEmojiReactionsDefaultProps}; diff --git a/src/components/Reactions/QuickEmojiReactions/BaseQuickEmojiReactions.tsx b/src/components/Reactions/QuickEmojiReactions/BaseQuickEmojiReactions.tsx new file mode 100644 index 000000000000..58973e90b9c4 --- /dev/null +++ b/src/components/Reactions/QuickEmojiReactions/BaseQuickEmojiReactions.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import type {Emoji} from '@assets/emojis/types'; +import AddReactionBubble from '@components/Reactions/AddReactionBubble'; +import EmojiReactionBubble from '@components/Reactions/EmojiReactionBubble'; +import Tooltip from '@components/Tooltip'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as EmojiUtils from '@libs/EmojiUtils'; +import * as Session from '@userActions/Session'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {BaseQuickEmojiReactionsOnyxProps, BaseQuickEmojiReactionsProps} from './types'; + +function BaseQuickEmojiReactions({ + reportAction, + onEmojiSelected, + preferredLocale = CONST.LOCALES.DEFAULT, + preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, + emojiReactions = {}, + onPressOpenPicker = () => {}, + onWillShowPicker = () => {}, +}: BaseQuickEmojiReactionsProps) { + const styles = useThemeStyles(); + + return ( + + {CONST.QUICK_REACTIONS.map((emoji: Emoji) => ( + + + onEmojiSelected(emoji, emojiReactions))} + /> + + + ))} + onEmojiSelected(emoji, emojiReactions)} + reportAction={reportAction} + /> + + ); +} + +BaseQuickEmojiReactions.displayName = 'BaseQuickEmojiReactions'; + +export default withOnyx({ + preferredSkinTone: { + key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, + }, + emojiReactions: { + key: ({reportActionID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`, + }, + preferredLocale: { + key: ONYXKEYS.NVP_PREFERRED_LOCALE, + }, +})(BaseQuickEmojiReactions); diff --git a/src/components/Reactions/QuickEmojiReactions/index.js b/src/components/Reactions/QuickEmojiReactions/index.js deleted file mode 100644 index 0366071f9c81..000000000000 --- a/src/components/Reactions/QuickEmojiReactions/index.js +++ /dev/null @@ -1,38 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {contextMenuRef} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; -import CONST from '@src/CONST'; -import BaseQuickEmojiReactions, {baseQuickEmojiReactionsPropTypes} from './BaseQuickEmojiReactions'; - -const propTypes = { - ...baseQuickEmojiReactionsPropTypes, - - /** - * Function that can be called to close the - * context menu in which this component is - * rendered. - */ - closeContextMenu: PropTypes.func.isRequired, -}; - -function QuickEmojiReactions(props) { - const onPressOpenPicker = (openPicker) => { - openPicker(contextMenuRef.current.contentRef, { - horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, - vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, - }); - }; - - return ( - - ); -} - -QuickEmojiReactions.displayName = 'QuickEmojiReactions'; -QuickEmojiReactions.propTypes = propTypes; -export default QuickEmojiReactions; diff --git a/src/components/Reactions/QuickEmojiReactions/index.native.js b/src/components/Reactions/QuickEmojiReactions/index.native.tsx similarity index 56% rename from src/components/Reactions/QuickEmojiReactions/index.native.js rename to src/components/Reactions/QuickEmojiReactions/index.native.tsx index c29bd2665e4a..b0eb88b31b68 100644 --- a/src/components/Reactions/QuickEmojiReactions/index.native.js +++ b/src/components/Reactions/QuickEmojiReactions/index.native.tsx @@ -1,41 +1,30 @@ -import PropTypes from 'prop-types'; import React from 'react'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import BaseQuickEmojiReactions, {baseQuickEmojiReactionsPropTypes} from './BaseQuickEmojiReactions'; +import BaseQuickEmojiReactions from './BaseQuickEmojiReactions'; +import type {OpenPickerCallback, QuickEmojiReactionsProps} from './types'; -const propTypes = { - ...baseQuickEmojiReactionsPropTypes, - - /** - * Function that can be called to close the - * context menu in which this component is - * rendered. - */ - closeContextMenu: PropTypes.func.isRequired, -}; - -function QuickEmojiReactions(props) { - const onPressOpenPicker = (openPicker) => { +function QuickEmojiReactions({closeContextMenu, ...rest}: QuickEmojiReactionsProps) { + const onPressOpenPicker = (openPicker?: OpenPickerCallback) => { // We first need to close the menu as it's a popover. // The picker is a popover as well and on mobile there can only // be one active popover at a time. - props.closeContextMenu(() => { + closeContextMenu(() => { // As the menu which includes the button to open the emoji picker // gets closed, before the picker actually opens, we pass the composer // ref as anchor for the emoji picker popover. - openPicker(ReportActionComposeFocusManager.composerRef); + openPicker?.(ReportActionComposeFocusManager.composerRef); }); }; return ( ); } QuickEmojiReactions.displayName = 'QuickEmojiReactions'; -QuickEmojiReactions.propTypes = propTypes; + export default QuickEmojiReactions; diff --git a/src/components/Reactions/QuickEmojiReactions/index.tsx b/src/components/Reactions/QuickEmojiReactions/index.tsx new file mode 100644 index 000000000000..3b44f4fe4826 --- /dev/null +++ b/src/components/Reactions/QuickEmojiReactions/index.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import {contextMenuRef} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import BaseQuickEmojiReactions from './BaseQuickEmojiReactions'; +import type {OpenPickerCallback, QuickEmojiReactionsProps} from './types'; + +function QuickEmojiReactions({closeContextMenu, ...rest}: QuickEmojiReactionsProps) { + const onPressOpenPicker = (openPicker?: OpenPickerCallback) => { + openPicker?.(contextMenuRef.current?.contentRef, { + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, + }); + }; + + return ( + + ); +} + +QuickEmojiReactions.displayName = 'QuickEmojiReactions'; + +export default QuickEmojiReactions; diff --git a/src/components/Reactions/QuickEmojiReactions/types.ts b/src/components/Reactions/QuickEmojiReactions/types.ts new file mode 100644 index 000000000000..d782d5ae35c7 --- /dev/null +++ b/src/components/Reactions/QuickEmojiReactions/types.ts @@ -0,0 +1,56 @@ +import type {RefObject} from 'react'; +import type {TextInput, View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {Emoji} from '@assets/emojis/types'; +import type {AnchorOrigin} from '@userActions/EmojiPickerAction'; +import type {Locale, ReportAction, ReportActionReactions} from '@src/types/onyx'; + +type PickerRefElement = RefObject; + +type OpenPickerCallback = (element?: PickerRefElement, anchorOrigin?: AnchorOrigin) => void; + +type CloseContextMenuCallback = () => void; + +type BaseQuickEmojiReactionsOnyxProps = { + /** All the emoji reactions for the report action. */ + emojiReactions: OnyxEntry; + + /** The user's preferred locale. */ + preferredLocale: OnyxEntry; + + /** The user's preferred skin tone. */ + preferredSkinTone: OnyxEntry; +}; + +type BaseQuickEmojiReactionsProps = BaseQuickEmojiReactionsOnyxProps & { + /** Callback to fire when an emoji is selected. */ + onEmojiSelected: (emoji: Emoji, emojiReactions: OnyxEntry) => void; + + /** + * Will be called when the emoji picker is about to show. + */ + onWillShowPicker?: (callback: CloseContextMenuCallback) => void; + + /** + * Callback to fire when the "open emoji picker" button is pressed. + * The function receives an argument which can be called + * to actually open the emoji picker. + */ + onPressOpenPicker?: (openPicker?: OpenPickerCallback) => void; + + /** ReportAction for EmojiPicker. */ + reportAction: ReportAction; + + /** Id of the ReportAction for EmojiPicker. */ + reportActionID: string; +}; + +type QuickEmojiReactionsProps = BaseQuickEmojiReactionsProps & { + /** + * Function that can be called to close the context menu + * in which this component is rendered. + */ + closeContextMenu: (callback: CloseContextMenuCallback) => void; +}; + +export type {BaseQuickEmojiReactionsProps, BaseQuickEmojiReactionsOnyxProps, QuickEmojiReactionsProps, OpenPickerCallback, CloseContextMenuCallback, PickerRefElement}; diff --git a/src/components/Reactions/ReactionTooltipContent.js b/src/components/Reactions/ReactionTooltipContent.js deleted file mode 100644 index bb6b03c5918b..000000000000 --- a/src/components/Reactions/ReactionTooltipContent.js +++ /dev/null @@ -1,67 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {useMemo} from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; -import Text from '@components/Text'; -import {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; -import withLocalize from '@components/withLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; - -const propTypes = { - /** - * A list of emoji codes to display in the tooltip. - */ - emojiCodes: PropTypes.arrayOf(PropTypes.string).isRequired, - - /** - * The name of the emoji to display in the tooltip. - */ - emojiName: PropTypes.string.isRequired, - - /** - * A list of account IDs to display in the tooltip. - */ - accountIDs: PropTypes.arrayOf(PropTypes.number).isRequired, - - ...withCurrentUserPersonalDetailsPropTypes, -}; - -const defaultProps = { - ...withCurrentUserPersonalDetailsDefaultProps, -}; - -function ReactionTooltipContent(props) { - const styles = useThemeStyles(); - const users = useMemo( - () => PersonalDetailsUtils.getPersonalDetailsByIDs(props.accountIDs, props.currentUserPersonalDetails.accountID, true), - [props.currentUserPersonalDetails.accountID, props.accountIDs], - ); - const namesString = _.filter( - _.map(users, (user) => user && user.displayName), - (n) => n, - ).join(', '); - return ( - - - {_.map(props.emojiCodes, (emojiCode) => ( - - {emojiCode} - - ))} - - - {namesString} - - {`${props.translate('emojiReactions.reactedWith')} :${props.emojiName}:`} - - ); -} - -ReactionTooltipContent.propTypes = propTypes; -ReactionTooltipContent.defaultProps = defaultProps; -ReactionTooltipContent.displayName = 'ReactionTooltipContent'; -export default React.memo(withLocalize(ReactionTooltipContent)); diff --git a/src/components/Reactions/ReactionTooltipContent.tsx b/src/components/Reactions/ReactionTooltipContent.tsx new file mode 100644 index 000000000000..198eba1f969c --- /dev/null +++ b/src/components/Reactions/ReactionTooltipContent.tsx @@ -0,0 +1,58 @@ +import React, {useMemo} from 'react'; +import {View} from 'react-native'; +import Text from '@components/Text'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; + +type ReactionTooltipContentProps = Pick & { + /** + * A list of emoji codes to display in the tooltip. + */ + emojiCodes: string[]; + + /** + * The name of the emoji to display in the tooltip. + */ + emojiName: string; + + /** + * A list of account IDs to display in the tooltip. + */ + accountIDs: number[]; +}; + +function ReactionTooltipContent({accountIDs, currentUserPersonalDetails = {}, emojiCodes, emojiName}: ReactionTooltipContentProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const users = useMemo(() => PersonalDetailsUtils.getPersonalDetailsByIDs(accountIDs, currentUserPersonalDetails.accountID, true), [currentUserPersonalDetails.accountID, accountIDs]); + + const namesString = users + .map((user) => user?.displayName) + .filter((name) => name) + .join(', '); + + return ( + + + {emojiCodes.map((emojiCode) => ( + + {emojiCode} + + ))} + + + {namesString} + + {`${translate('emojiReactions.reactedWith')} :${emojiName}:`} + + ); +} + +ReactionTooltipContent.displayName = 'ReactionTooltipContent'; + +export default React.memo(ReactionTooltipContent); diff --git a/src/components/Reactions/ReportActionItemEmojiReactions.js b/src/components/Reactions/ReportActionItemEmojiReactions.tsx similarity index 52% rename from src/components/Reactions/ReportActionItemEmojiReactions.js rename to src/components/Reactions/ReportActionItemEmojiReactions.tsx index 547f4089857f..d1a2cf56b6a5 100644 --- a/src/components/Reactions/ReportActionItemEmojiReactions.js +++ b/src/components/Reactions/ReportActionItemEmojiReactions.tsx @@ -1,63 +1,98 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import sortBy from 'lodash/sortBy'; import React, {useContext, useRef} from 'react'; import {View} from 'react-native'; -import _ from 'underscore'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {Emoji} from '@assets/emojis/types'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Tooltip from '@components/Tooltip'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; -import withLocalize from '@components/withLocalize'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import * as EmojiUtils from '@libs/EmojiUtils'; -import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; import {ReactionListContext} from '@pages/home/ReportScreenContext'; +import type {ReactionListAnchor, ReactionListEvent} from '@pages/home/ReportScreenContext'; +import CONST from '@src/CONST'; +import type {Locale, ReportAction, ReportActionReactions} from '@src/types/onyx'; +import type {PendingAction} from '@src/types/onyx/OnyxCommon'; import AddReactionBubble from './AddReactionBubble'; import EmojiReactionBubble from './EmojiReactionBubble'; -import EmojiReactionsPropTypes from './EmojiReactionsPropTypes'; import ReactionTooltipContent from './ReactionTooltipContent'; -const propTypes = { - emojiReactions: EmojiReactionsPropTypes, +type ReportActionItemEmojiReactionsProps = WithCurrentUserPersonalDetailsProps & { + /** All the emoji reactions for the report action. */ + emojiReactions: OnyxEntry; + + /** The user's preferred locale. */ + preferredLocale: OnyxEntry; /** The report action that these reactions are for */ - reportAction: PropTypes.shape(reportActionPropTypes).isRequired, + reportAction: ReportAction; /** * Function to call when the user presses on an emoji. * This can also be an emoji the user already reacted with, * hence this function asks to toggle the reaction by emoji. */ - toggleReaction: PropTypes.func.isRequired, + toggleReaction: (emoji: Emoji) => void; /** We disable reacting with emojis on report actions that have errors */ - shouldBlockReactions: PropTypes.bool, - - ...withCurrentUserPersonalDetailsPropTypes, + shouldBlockReactions?: boolean; }; -const defaultProps = { - ...withCurrentUserPersonalDetailsDefaultProps, - emojiReactions: {}, - shouldBlockReactions: false, +type PopoverReactionListAnchors = Record; + +type FormattedReaction = { + /** The emoji codes to display in the bubble */ + emojiCodes: string[]; + + /** IDs of users used the reaction */ + userAccountIDs: number[]; + + /** Total reaction count */ + reactionCount: number; + + /** Whether the current account has reacted to the report action */ + hasUserReacted: boolean; + + /** Oldest timestamp of when the emoji was added */ + oldestTimestamp: string; + + /** Callback to fire on press */ + onPress: () => void; + + /** Callback to fire on reaction list open */ + onReactionListOpen: (event: ReactionListEvent) => void; + + /** The name of the emoji */ + reactionEmojiName: string; + + /** The type of action that's pending */ + pendingAction: PendingAction; }; -function ReportActionItemEmojiReactions(props) { +function ReportActionItemEmojiReactions({ + reportAction, + currentUserPersonalDetails, + toggleReaction, + emojiReactions = {}, + shouldBlockReactions = false, + preferredLocale = CONST.LOCALES.DEFAULT, +}: ReportActionItemEmojiReactionsProps) { const styles = useThemeStyles(); const reactionListRef = useContext(ReactionListContext); - const popoverReactionListAnchors = useRef({}); + const popoverReactionListAnchors = useRef({}); let totalReactionCount = 0; - const reportAction = props.reportAction; const reportActionID = reportAction.reportActionID; - const formattedReactions = _.chain(props.emojiReactions) - .map((emojiReaction, emojiName) => { + // Each emoji is sorted by the oldest timestamp of user reactions so that they will always appear in the same order for everyone + const formattedReactions: Array = sortBy( + Object.entries(emojiReactions ?? {}).map(([emojiName, emojiReaction]) => { const {emoji, emojiCodes, reactionCount, hasUserReacted, userAccountIDs, oldestTimestamp} = EmojiUtils.getEmojiReactionDetails( emojiName, emojiReaction, - props.currentUserPersonalDetails.accountID, + currentUserPersonalDetails.accountID, ); if (reactionCount === 0) { @@ -66,11 +101,11 @@ function ReportActionItemEmojiReactions(props) { totalReactionCount += reactionCount; const onPress = () => { - props.toggleReaction(emoji); + toggleReaction(emoji); }; - const onReactionListOpen = (event) => { - reactionListRef.current.showReactionList(event, popoverReactionListAnchors.current[emojiName], emojiName, reportActionID); + const onReactionListOpen = (event: ReactionListEvent) => { + reactionListRef?.current?.showReactionList(event, popoverReactionListAnchors.current[emojiName], emojiName, reportActionID); }; return { @@ -84,15 +119,14 @@ function ReportActionItemEmojiReactions(props) { reactionEmojiName: emojiName, pendingAction: emojiReaction.pendingAction, }; - }) - // Each emoji is sorted by the oldest timestamp of user reactions so that they will always appear in the same order for everyone - .sortBy('oldestTimestamp') - .value(); + }), + ['oldestTimestamp'], + ); return ( totalReactionCount > 0 && ( - {_.map(formattedReactions, (reaction) => { + {formattedReactions.map((reaction) => { if (reaction === null) { return; } @@ -100,19 +134,19 @@ function ReportActionItemEmojiReactions(props) { ( )} - renderTooltipContentKey={[..._.map(reaction.userAccountIDs, String), ...reaction.emojiCodes]} + renderTooltipContentKey={[...reaction.userAccountIDs.map(String), ...reaction.emojiCodes]} key={reaction.reactionEmojiName} > (popoverReactionListAnchors.current[reaction.reactionEmojiName] = ref)} @@ -121,17 +155,17 @@ function ReportActionItemEmojiReactions(props) { onPress={reaction.onPress} hasUserReacted={reaction.hasUserReacted} onReactionListOpen={reaction.onReactionListOpen} - shouldBlockReactions={props.shouldBlockReactions} + shouldBlockReactions={shouldBlockReactions} /> ); })} - {!props.shouldBlockReactions && ( + {!shouldBlockReactions && ( )} @@ -140,6 +174,5 @@ function ReportActionItemEmojiReactions(props) { } ReportActionItemEmojiReactions.displayName = 'ReportActionItemReactions'; -ReportActionItemEmojiReactions.propTypes = propTypes; -ReportActionItemEmojiReactions.defaultProps = defaultProps; -export default compose(withLocalize, withCurrentUserPersonalDetails)(ReportActionItemEmojiReactions); + +export default withCurrentUserPersonalDetails(ReportActionItemEmojiReactions); diff --git a/src/libs/EmojiTrie.ts b/src/libs/EmojiTrie.ts index 4c441facdd46..d0f0b0dcfab6 100644 --- a/src/libs/EmojiTrie.ts +++ b/src/libs/EmojiTrie.ts @@ -1,38 +1,11 @@ import emojis, {localeEmojis} from '@assets/emojis'; +import type {Emoji, HeaderEmoji, PickerEmoji} from '@assets/emojis/types'; import CONST from '@src/CONST'; -import type IconAsset from '@src/types/utils/IconAsset'; import Timing from './actions/Timing'; import Trie from './Trie'; -type HeaderEmoji = { - code: string; - header: boolean; - icon: IconAsset; -}; - -type SimpleEmoji = { - code: string; - name: string; - types?: string[]; -}; - -type Emoji = HeaderEmoji | SimpleEmoji; - -type LocalizedEmoji = { - name?: string; - keywords: string[]; -}; - -type LocalizedEmojis = Record; - -type Suggestion = { - code: string; - types?: string[]; - name: string; -}; - type EmojiMetaData = { - suggestions?: Suggestion[]; + suggestions?: Emoji[]; code?: string; types?: string[]; name?: string; @@ -56,7 +29,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, keywords: string[], item: SimpleEmoji, name: string, shouldPrependKeyword = false) { +function addKeywordsToTrie(trie: Trie, keywords: string[], item: Emoji, name: string, shouldPrependKeyword = false) { keywords.forEach((keyword) => { const keywordNode = trie.search(keyword); if (!keywordNode) { @@ -85,13 +58,13 @@ function getNameParts(name: string): string[] { function createTrie(lang: SupportedLanguage = CONST.LOCALES.DEFAULT): Trie { const trie = new Trie(); - const langEmojis: LocalizedEmojis = localeEmojis[lang]; - const defaultLangEmojis: LocalizedEmojis = localeEmojis[CONST.LOCALES.DEFAULT]; + const langEmojis = localeEmojis[lang]; + const defaultLangEmojis = localeEmojis[CONST.LOCALES.DEFAULT]; const isDefaultLocale = lang === CONST.LOCALES.DEFAULT; emojis - .filter((item: Emoji): item is SimpleEmoji => !(item as HeaderEmoji).header) - .forEach((item: SimpleEmoji) => { + .filter((item: PickerEmoji): item is Emoji => !(item as HeaderEmoji).header) + .forEach((item: Emoji) => { const englishName = item.name; const localeName = langEmojis?.[item.code]?.name ?? englishName; @@ -127,4 +100,4 @@ const emojiTrie: EmojiTrie = supportedLanguages.reduce((prev, cur) => ({...prev, Timing.end(CONST.TIMING.TRIE_INITIALIZATION); export default emojiTrie; -export type {SimpleEmoji, SupportedLanguage}; +export type {SupportedLanguage}; diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 06bbd5c871ed..02d1b34c69c1 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -2,11 +2,12 @@ import {getUnixTime} from 'date-fns'; import Str from 'expensify-common/lib/str'; import memoize from 'lodash/memoize'; import Onyx from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import * as Emojis from '@assets/emojis'; import type {Emoji, HeaderEmoji, PickerEmojis} from '@assets/emojis/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {FrequentlyUsedEmoji} from '@src/types/onyx'; +import type {FrequentlyUsedEmoji, Locale} from '@src/types/onyx'; import type {ReportActionReaction, UsersReactions} from '@src/types/onyx/ReportActionReactions'; import type IconAsset from '@src/types/utils/IconAsset'; import type {SupportedLanguage} from './EmojiTrie'; @@ -48,13 +49,13 @@ const getEmojiName = (emoji: Emoji, lang: 'en' | 'es' = CONST.LOCALES.DEFAULT): /** * Given an English emoji name, get its localized version */ -const getLocalizedEmojiName = (name: string, lang: 'en' | 'es'): string => { +const getLocalizedEmojiName = (name: string, lang: OnyxEntry): string => { if (lang === CONST.LOCALES.DEFAULT) { return name; } const emojiCode = Emojis.emojiNameTable[name]?.code ?? ''; - return Emojis.localeEmojis[lang]?.[emojiCode]?.name ?? ''; + return (lang && Emojis.localeEmojis[lang]?.[emojiCode]?.name) ?? ''; }; /** @@ -438,8 +439,8 @@ const getPreferredSkinToneIndex = (value: string | number | null): number => { * Given an emoji object it returns the correct emoji code * based on the users preferred skin tone. */ -const getPreferredEmojiCode = (emoji: Emoji, preferredSkinTone: number): string => { - if (emoji.types) { +const getPreferredEmojiCode = (emoji: Emoji, preferredSkinTone: OnyxEntry): string => { + if (emoji.types && typeof preferredSkinTone === 'number') { const emojiCodeWithSkinTone = emoji.types[preferredSkinTone]; // Note: it can happen that preferredSkinTone has a outdated format, diff --git a/src/libs/actions/EmojiPickerAction.ts b/src/libs/actions/EmojiPickerAction.ts index 064c52f9b7ef..56a5f34c0b8e 100644 --- a/src/libs/actions/EmojiPickerAction.ts +++ b/src/libs/actions/EmojiPickerAction.ts @@ -1,6 +1,9 @@ import React from 'react'; -import type {View} from 'react-native'; +import type {MutableRefObject} from 'react'; +import type {TextInput, View} from 'react-native'; import type {ValueOf} from 'type-fest'; +import type {Emoji} from '@assets/emojis/types'; +import type {CloseContextMenuCallback} from '@components/Reactions/QuickEmojiReactions/types'; import type CONST from '@src/CONST'; type AnchorOrigin = { @@ -8,23 +11,31 @@ type AnchorOrigin = { vertical: ValueOf; }; +type EmojiPopoverAnchor = MutableRefObject; + +type OnWillShowPicker = (callback: CloseContextMenuCallback) => void; + +type OnModalHideValue = () => void; + // TODO: Move this type to src/components/EmojiPicker/EmojiPicker.js once it is converted to TS type EmojiPickerRef = { showEmojiPicker: ( - onModalHideValue?: () => void, - onEmojiSelectedValue?: () => void, - emojiPopoverAnchor?: React.MutableRefObject, + onModalHideValue: OnModalHideValue, + onEmojiSelectedValue: OnEmojiSelected, + emojiPopoverAnchor: EmojiPopoverAnchor, anchorOrigin?: AnchorOrigin, - onWillShow?: () => void, + onWillShow?: OnWillShowPicker, id?: string, ) => void; isActive: (id: string) => boolean; clearActive: () => void; - hideEmojiPicker: (isNavigating: boolean) => void; + hideEmojiPicker: (isNavigating?: boolean) => void; isEmojiPickerVisible: boolean; resetEmojiPopoverAnchor: () => void; }; +type OnEmojiSelected = (emojiCode: string, emojiObject: Emoji) => void; + const emojiPickerRef = React.createRef(); /** @@ -37,7 +48,14 @@ const emojiPickerRef = React.createRef(); * @param onWillShow - Run a callback when Popover will show * @param id - Unique id for EmojiPicker */ -function showEmojiPicker(onModalHide = () => {}, onEmojiSelected = () => {}, emojiPopoverAnchor = undefined, anchorOrigin = undefined, onWillShow = () => {}, id = undefined) { +function showEmojiPicker( + onModalHide: OnModalHideValue, + onEmojiSelected: OnEmojiSelected, + emojiPopoverAnchor: EmojiPopoverAnchor, + anchorOrigin?: AnchorOrigin, + onWillShow: OnWillShowPicker = () => {}, + id?: string, +) { if (!emojiPickerRef.current) { return; } @@ -92,3 +110,4 @@ function resetEmojiPopoverAnchor() { } export {emojiPickerRef, showEmojiPicker, hideEmojiPicker, isActive, clearActive, isEmojiPickerVisible, resetEmojiPopoverAnchor}; +export type {AnchorOrigin}; diff --git a/src/pages/home/ReportScreenContext.ts b/src/pages/home/ReportScreenContext.ts index 98d593b92d91..3b4e574e01a1 100644 --- a/src/pages/home/ReportScreenContext.ts +++ b/src/pages/home/ReportScreenContext.ts @@ -1,9 +1,13 @@ import type {RefObject} from 'react'; import {createContext} from 'react'; -import type {FlatList, GestureResponderEvent} from 'react-native'; +import type {FlatList, GestureResponderEvent, View} from 'react-native'; + +type ReactionListAnchor = View | HTMLDivElement | null; + +type ReactionListEvent = GestureResponderEvent | MouseEvent; type ReactionListRef = { - showReactionList: (event: GestureResponderEvent | undefined, reactionListAnchor: Element, emojiName: string, reportActionID: string) => void; + showReactionList: (event: ReactionListEvent | undefined, reactionListAnchor: ReactionListAnchor, emojiName: string, reportActionID: string) => void; hideReactionList: () => void; isActiveReportAction: (actionID: number | string) => boolean; }; @@ -21,4 +25,4 @@ const ActionListContext = createContext({flatListRef: nul const ReactionListContext = createContext(null); export {ActionListContext, ReactionListContext}; -export type {ReactionListRef, ActionListContextType, ReactionListContextType, FlatListRefType}; +export type {ReactionListRef, ActionListContextType, ReactionListContextType, FlatListRefType, ReactionListAnchor, ReactionListEvent}; diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts index 317c3846d160..76af9d4fccb0 100644 --- a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts +++ b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts @@ -1,5 +1,6 @@ import React from 'react'; -import type {GestureResponderEvent, Text as RNText} from 'react-native'; +import type {RefObject} from 'react'; +import type {GestureResponderEvent, Text as RNText, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; @@ -39,6 +40,7 @@ type ReportActionContextMenu = { instanceID: string; runAndResetOnPopoverHide: () => void; clearActiveReportAction: () => void; + contentRef: RefObject; }; const contextMenuRef = React.createRef(); diff --git a/src/types/onyx/FrequentlyUsedEmoji.ts b/src/types/onyx/FrequentlyUsedEmoji.ts index 333721b25b52..c8f6a5179fc6 100644 --- a/src/types/onyx/FrequentlyUsedEmoji.ts +++ b/src/types/onyx/FrequentlyUsedEmoji.ts @@ -12,7 +12,7 @@ type FrequentlyUsedEmoji = { lastUpdatedAt: number; /** The emoji skin tone type */ - types?: string[]; + types?: readonly string[]; /** The emoji keywords */ keywords?: string[]; diff --git a/src/types/onyx/ReportActionReactions.ts b/src/types/onyx/ReportActionReactions.ts index be117aafc4c5..0173fcf244f5 100644 --- a/src/types/onyx/ReportActionReactions.ts +++ b/src/types/onyx/ReportActionReactions.ts @@ -24,7 +24,7 @@ type ReportActionReaction = { users: UsersReactions; /** Is this action pending? */ - pendingAction?: OnyxCommon.PendingAction; + pendingAction: OnyxCommon.PendingAction; }; type ReportActionReactions = Record;