diff --git a/src/components/PressableWithSecondaryInteraction/index.js b/src/components/PressableWithSecondaryInteraction/index.js
index 771373293525..1096ce474048 100644
--- a/src/components/PressableWithSecondaryInteraction/index.js
+++ b/src/components/PressableWithSecondaryInteraction/index.js
@@ -31,7 +31,7 @@ class PressableWithSecondaryInteraction extends Component {
* @param {Event} e - the secondary interaction event
*/
executeSecondaryInteraction(e) {
- if (DeviceCapabilities.hasHoverSupport()) {
+ if (DeviceCapabilities.hasHoverSupport() && !this.props.enableLongPressWithHover) {
return;
}
if (this.props.withoutFocusOnSecondaryInteraction && this.pressableRef) {
diff --git a/src/components/PressableWithSecondaryInteraction/pressableWithSecondaryInteractionPropTypes.js b/src/components/PressableWithSecondaryInteraction/pressableWithSecondaryInteractionPropTypes.js
index 12d44ddd5d81..c83761e73c1d 100644
--- a/src/components/PressableWithSecondaryInteraction/pressableWithSecondaryInteractionPropTypes.js
+++ b/src/components/PressableWithSecondaryInteraction/pressableWithSecondaryInteractionPropTypes.js
@@ -53,6 +53,7 @@ const defaultProps = {
preventDefaultContextMenu: true,
inline: false,
withoutFocusOnSecondaryInteraction: false,
+ enableLongPressWithHover: false,
};
export {propTypes, defaultProps};
diff --git a/src/components/Reactions/EmojiReactionBubble.js b/src/components/Reactions/EmojiReactionBubble.js
index f1b87d48de2a..dda149e90c21 100644
--- a/src/components/Reactions/EmojiReactionBubble.js
+++ b/src/components/Reactions/EmojiReactionBubble.js
@@ -1,14 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
-import {Pressable} from 'react-native';
import styles from '../../styles/styles';
import Text from '../Text';
import * as StyleUtils from '../../styles/StyleUtils';
-import withCurrentUserPersonalDetails, {
- withCurrentUserPersonalDetailsDefaultProps,
- withCurrentUserPersonalDetailsPropTypes,
-} from '../withCurrentUserPersonalDetails';
-import * as Report from '../../libs/actions/Report';
+import PressableWithSecondaryInteraction from '../PressableWithSecondaryInteraction';
+import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions';
+import {withCurrentUserPersonalDetailsDefaultProps} from '../withCurrentUserPersonalDetails';
const propTypes = {
/**
@@ -32,64 +29,66 @@ const propTypes = {
*/
count: PropTypes.number,
- /**
- * The account ids of the users who reacted.
- */
- reactionUsers: PropTypes.arrayOf(PropTypes.string),
-
/** Whether it is for context menu so we can modify its style */
isContextMenu: PropTypes.bool,
- ...withCurrentUserPersonalDetailsPropTypes,
+ /**
+ * Returns true if the current account has reacted to the report action (with the given skin tone).
+ */
+ hasUserReacted: PropTypes.bool,
+
+ ...windowDimensionsPropTypes,
};
const defaultProps = {
count: 0,
onReactionListOpen: () => {},
- reactionUsers: [],
isContextMenu: false,
...withCurrentUserPersonalDetailsDefaultProps,
};
-const EmojiReactionBubble = (props) => {
- const hasUserReacted = Report.hasAccountIDReacted(props.currentUserPersonalDetails.accountID, props.reactionUsers);
- return (
- [
- styles.emojiReactionBubble,
- StyleUtils.getEmojiReactionBubbleStyle(hovered || pressed, hasUserReacted, props.isContextMenu),
- ]}
- onPress={props.onPress}
- onLongPress={props.onReactionListOpen}
+const EmojiReactionBubble = props => (
+ [
+ styles.emojiReactionBubble,
+ StyleUtils.getEmojiReactionBubbleStyle(hovered || pressed, props.hasUserReacted, props.isContextMenu),
+ ]}
+ onPress={props.onPress}
+ onLongPress={props.onReactionListOpen}
+ onSecondaryInteraction={props.onReactionListOpen}
+ ref={props.forwardedRef}
+ enableLongPressWithHover={props.isSmallScreenWidth}
- // Prevent text input blur when emoji reaction is clicked
- onMouseDown={e => e.preventDefault()}
+ // Prevent text input blur when emoji reaction is clicked
+ onMouseDown={e => e.preventDefault()}
+ >
+
-
- {props.emojiCodes.join('')}
-
- {props.count > 0 && (
+ {props.emojiCodes.join('')}
+
+ {props.count > 0 && (
{props.count}
- )}
-
- );
-};
+ )}
+
+);
EmojiReactionBubble.propTypes = propTypes;
EmojiReactionBubble.defaultProps = defaultProps;
EmojiReactionBubble.displayName = 'EmojiReactionBubble';
-export default withCurrentUserPersonalDetails(EmojiReactionBubble);
+export default withWindowDimensions(React.forwardRef((props, ref) => (
+ /* eslint-disable-next-line react/jsx-props-no-spreading */
+
+)));
diff --git a/src/components/Reactions/ReportActionItemReactions.js b/src/components/Reactions/ReportActionItemReactions.js
index 66198e3b7cf2..2eddd6d4f1a6 100644
--- a/src/components/Reactions/ReportActionItemReactions.js
+++ b/src/components/Reactions/ReportActionItemReactions.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, {useRef} from 'react';
import _ from 'underscore';
import {View} from 'react-native';
import PropTypes from 'prop-types';
@@ -6,7 +6,11 @@ import styles from '../../styles/styles';
import EmojiReactionBubble from './EmojiReactionBubble';
import emojis from '../../../assets/emojis';
import AddReactionBubble from './AddReactionBubble';
+import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../withCurrentUserPersonalDetails';
import getPreferredEmojiCode from './getPreferredEmojiCode';
+import * as PersonalDetailsUtils from '../../libs/PersonalDetailsUtils';
+import * as Report from '../../libs/actions/Report';
+import * as ReactionList from '../../pages/home/report/ReactionList/ReactionList';
import Tooltip from '../Tooltip';
import ReactionTooltipContent from './ReactionTooltipContent';
@@ -52,22 +56,46 @@ const propTypes = {
* hence this function asks to toggle the reaction by emoji.
*/
toggleReaction: PropTypes.func.isRequired,
+
+ ...withCurrentUserPersonalDetailsPropTypes,
+};
+
+const defaultProps = {
+ ...withCurrentUserPersonalDetailsDefaultProps,
};
const ReportActionItemReactions = (props) => {
+ const popoverReactionListAnchor = useRef(null);
const reactionsWithCount = _.filter(props.reactions, reaction => reaction.users.length > 0);
return (
-
+
+
{_.map(reactionsWithCount, (reaction) => {
const reactionCount = reaction.users.length;
const reactionUsers = _.map(reaction.users, sender => sender.accountID.toString());
const emoji = _.find(emojis, e => e.name === reaction.emoji);
const emojiCodes = getUniqueEmojiCodes(emoji, reaction.users);
+ const hasUserReacted = Report.hasAccountIDReacted(props.currentUserPersonalDetails.accountID, reactionUsers);
const onPress = () => {
props.toggleReaction(emoji);
};
+ const onReactionListOpen = (event) => {
+ const users = PersonalDetailsUtils.getPersonalDetailsByIDs(reactionUsers);
+ ReactionList.showReactionList(
+ event,
+ popoverReactionListAnchor.current,
+ users,
+ reaction.emoji,
+ emojiCodes,
+ reactionCount,
+ hasUserReacted,
+ );
+ };
return (
{
key={reaction.emoji}
>
+
);
})}
{reactionsWithCount.length > 0 && }
@@ -96,4 +128,6 @@ const ReportActionItemReactions = (props) => {
ReportActionItemReactions.displayName = 'ReportActionItemReactions';
ReportActionItemReactions.propTypes = propTypes;
-export default ReportActionItemReactions;
+ReportActionItemReactions.defaultProps = defaultProps;
+export default withCurrentUserPersonalDetails(ReportActionItemReactions);
+
diff --git a/src/pages/home/report/ReactionList/BaseReactionList.js b/src/pages/home/report/ReactionList/BaseReactionList.js
new file mode 100755
index 000000000000..0fbf8d8fbb51
--- /dev/null
+++ b/src/pages/home/report/ReactionList/BaseReactionList.js
@@ -0,0 +1,117 @@
+/* eslint-disable rulesdir/onyx-props-must-have-default */
+import React from 'react';
+import {FlatList} from 'react-native';
+import PropTypes from 'prop-types';
+import Str from 'expensify-common/lib/str';
+import styles from '../../../../styles/styles';
+import HeaderReactionList from './HeaderReactionList';
+import * as ReportUtils from '../../../../libs/ReportUtils';
+import CONST from '../../../../CONST';
+import participantPropTypes from '../../../../components/participantPropTypes';
+import reactionPropTypes from './reactionPropTypes';
+import OptionRow from '../../../../components/OptionRow';
+import variables from '../../../../styles/variables';
+import withWindowDimensions from '../../../../components/withWindowDimensions';
+
+const propTypes = {
+
+ /**
+ * Array of personal detail objects
+ */
+ users: PropTypes.arrayOf(participantPropTypes).isRequired,
+
+ /**
+ * Returns true if the current account has reacted to the report action (with the given skin tone).
+ */
+ hasUserReacted: PropTypes.bool,
+
+ ...reactionPropTypes,
+};
+
+const defaultProps = {
+ hasUserReacted: false,
+};
+
+/**
+ * Given an emoji item object, render a component based on its type.
+ * Items with the code "SPACER" return nothing and are used to fill rows up to 8
+ * so that the sticky headers function properly
+ *
+ * @param {Object} params
+ * @param {Object} params.item
+ * @return {React.Component}
+ */
+const renderItem = ({item}) => (
+
+);
+
+/**
+ * Create a unique key for each action in the FlatList.
+ * @param {Object} item
+ * @param {Number} index
+ * @return {String}
+ */
+const keyExtractor = (item, index) => `${item.login}+${index}`;
+
+/**
+ * This function will be used with FlatList getItemLayout property for optimization purpose that allows skipping
+ * the measurement of dynamic content if we know the size (height or width) of items ahead of time.
+ * Generate and return an object with properties length(height of each individual row),
+ * offset(distance of the current row from the top of the FlatList), index(current row index)
+ *
+ * @param {*} _ FlatList item
+ * @param {Number} index row index
+ * @returns {Object}
+ */
+const getItemLayout = (_, index) => ({
+ index,
+ length: variables.listItemHeightNormal,
+ offset: variables.listItemHeightNormal * index,
+});
+
+const BaseReactionList = (props) => {
+ if (!props.isVisible) {
+ return null;
+ }
+ return (
+ <>
+
+
+ >
+ );
+};
+
+BaseReactionList.propTypes = propTypes;
+BaseReactionList.defaultProps = defaultProps;
+BaseReactionList.displayName = 'BaseReactionList';
+
+export default withWindowDimensions(BaseReactionList);
diff --git a/src/pages/home/report/ReactionList/HeaderReactionList.js b/src/pages/home/report/ReactionList/HeaderReactionList.js
new file mode 100644
index 000000000000..b3551aff4f60
--- /dev/null
+++ b/src/pages/home/report/ReactionList/HeaderReactionList.js
@@ -0,0 +1,63 @@
+import React from 'react';
+import {View, TouchableOpacity} from 'react-native';
+import PropTypes from 'prop-types';
+import styles from '../../../../styles/styles';
+import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize';
+import Text from '../../../../components/Text';
+import Icon from '../../../../components/Icon';
+import * as Expensicons from '../../../../components/Icon/Expensicons';
+import * as StyleUtils from '../../../../styles/StyleUtils';
+import reactionPropTypes from './reactionPropTypes';
+import compose from '../../../../libs/compose';
+import withWindowDimensions, {windowDimensionsPropTypes} from '../../../../components/withWindowDimensions';
+
+const propTypes = {
+ ...reactionPropTypes,
+ ...withLocalizePropTypes,
+ ...windowDimensionsPropTypes,
+
+ /**
+ * Returns true if the current account has reacted to the report action (with the given skin tone).
+ */
+ hasUserReacted: PropTypes.bool,
+};
+
+const defaultProps = {
+ hasUserReacted: false,
+};
+
+const HeaderReactionList = props => (
+
+
+
+
+ {props.emojiCodes.join('')}
+
+
+ {props.emojiCount}
+
+
+ {`:${props.emojiName}:`}
+
+
+ {props.isSmallScreenWidth && (
+
+
+
+ )}
+
+);
+
+HeaderReactionList.propTypes = propTypes;
+HeaderReactionList.defaultProps = defaultProps;
+HeaderReactionList.displayName = 'HeaderReactionList';
+
+export default compose(
+ withWindowDimensions,
+ withLocalize,
+)(HeaderReactionList);
+
diff --git a/src/pages/home/report/ReactionList/PopoverReactionList.js b/src/pages/home/report/ReactionList/PopoverReactionList.js
new file mode 100644
index 000000000000..c1a827de1e61
--- /dev/null
+++ b/src/pages/home/report/ReactionList/PopoverReactionList.js
@@ -0,0 +1,212 @@
+import React from 'react';
+import {Dimensions} from 'react-native';
+
+import lodashGet from 'lodash/get';
+import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize';
+import PopoverWithMeasuredContent from '../../../../components/PopoverWithMeasuredContent';
+
+import BaseReactionList from './BaseReactionList';
+
+const propTypes = {
+ ...withLocalizePropTypes,
+};
+
+class PopoverReactionList extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ isPopoverVisible: false,
+ cursorRelativePosition: {
+ horizontal: 0,
+ vertical: 0,
+ },
+
+ // The horizontal and vertical position (relative to the screen) where the popover will display.
+ popoverAnchorPosition: {
+ horizontal: 0,
+ vertical: 0,
+ },
+ users: [],
+ emojiCodes: [],
+ emojiName: '',
+ emojiCount: 0,
+ hasUserReacted: false,
+ };
+
+ this.onPopoverHideActionCallback = () => {};
+ this.reactionListAnchor = undefined;
+ this.showReactionList = this.showReactionList.bind(this);
+
+ this.hideReactionList = this.hideReactionList.bind(this);
+ this.measureContent = this.measureContent.bind(this);
+ this.measureReactionListPosition = this.measureReactionListPosition.bind(this);
+ this.getReactionListMeasuredLocation = this.getReactionListMeasuredLocation.bind(this);
+
+ this.dimensionsEventListener = null;
+
+ this.contentRef = React.createRef();
+ }
+
+ componentDidMount() {
+ this.dimensionsEventListener = Dimensions.addEventListener('change', this.measureReactionListPosition);
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ const previousLocale = lodashGet(this.props, 'preferredLocale', 'en');
+ const nextLocale = lodashGet(nextProps, 'preferredLocale', 'en');
+ return this.state.isPopoverVisible !== nextState.isPopoverVisible
+ || this.state.popoverAnchorPosition !== nextState.popoverAnchorPosition
+ || previousLocale !== nextLocale;
+ }
+
+ componentWillUnmount() {
+ if (!this.dimensionsEventListener) {
+ return;
+ }
+ this.dimensionsEventListener.remove();
+ }
+
+ /**
+ * Get the PopoverReactionList anchor position
+ * We calculate the achor coordinates from measureInWindow async method
+ *
+ * @returns {Promise