Skip to content

Commit 75d03a4

Browse files
authored
Merge pull request #15685 from margelo/perunt/reaction-list-on-secondary-interaction
Detailed list of reaction senders when long-/right-pressing a reaction
2 parents bfe7195 + 3604015 commit 75d03a4

File tree

13 files changed

+583
-45
lines changed

13 files changed

+583
-45
lines changed

src/components/PressableWithSecondaryInteraction/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class PressableWithSecondaryInteraction extends Component {
3131
* @param {Event} e - the secondary interaction event
3232
*/
3333
executeSecondaryInteraction(e) {
34-
if (DeviceCapabilities.hasHoverSupport()) {
34+
if (DeviceCapabilities.hasHoverSupport() && !this.props.enableLongPressWithHover) {
3535
return;
3636
}
3737
if (this.props.withoutFocusOnSecondaryInteraction && this.pressableRef) {

src/components/PressableWithSecondaryInteraction/pressableWithSecondaryInteractionPropTypes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ const defaultProps = {
5353
preventDefaultContextMenu: true,
5454
inline: false,
5555
withoutFocusOnSecondaryInteraction: false,
56+
enableLongPressWithHover: false,
5657
};
5758

5859
export {propTypes, defaultProps};
Lines changed: 39 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
3-
import {Pressable} from 'react-native';
43
import styles from '../../styles/styles';
54
import Text from '../Text';
65
import * as StyleUtils from '../../styles/StyleUtils';
7-
import withCurrentUserPersonalDetails, {
8-
withCurrentUserPersonalDetailsDefaultProps,
9-
withCurrentUserPersonalDetailsPropTypes,
10-
} from '../withCurrentUserPersonalDetails';
11-
import * as Report from '../../libs/actions/Report';
6+
import PressableWithSecondaryInteraction from '../PressableWithSecondaryInteraction';
7+
import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions';
8+
import {withCurrentUserPersonalDetailsDefaultProps} from '../withCurrentUserPersonalDetails';
129

1310
const propTypes = {
1411
/**
@@ -32,64 +29,66 @@ const propTypes = {
3229
*/
3330
count: PropTypes.number,
3431

35-
/**
36-
* The account ids of the users who reacted.
37-
*/
38-
reactionUsers: PropTypes.arrayOf(PropTypes.string),
39-
4032
/** Whether it is for context menu so we can modify its style */
4133
isContextMenu: PropTypes.bool,
4234

43-
...withCurrentUserPersonalDetailsPropTypes,
35+
/**
36+
* Returns true if the current account has reacted to the report action (with the given skin tone).
37+
*/
38+
hasUserReacted: PropTypes.bool,
39+
40+
...windowDimensionsPropTypes,
4441
};
4542

4643
const defaultProps = {
4744
count: 0,
4845
onReactionListOpen: () => {},
49-
reactionUsers: [],
5046
isContextMenu: false,
5147

5248
...withCurrentUserPersonalDetailsDefaultProps,
5349
};
5450

55-
const EmojiReactionBubble = (props) => {
56-
const hasUserReacted = Report.hasAccountIDReacted(props.currentUserPersonalDetails.accountID, props.reactionUsers);
57-
return (
58-
<Pressable
59-
style={({hovered, pressed}) => [
60-
styles.emojiReactionBubble,
61-
StyleUtils.getEmojiReactionBubbleStyle(hovered || pressed, hasUserReacted, props.isContextMenu),
62-
]}
63-
onPress={props.onPress}
64-
onLongPress={props.onReactionListOpen}
51+
const EmojiReactionBubble = props => (
52+
<PressableWithSecondaryInteraction
53+
style={({hovered, pressed}) => [
54+
styles.emojiReactionBubble,
55+
StyleUtils.getEmojiReactionBubbleStyle(hovered || pressed, props.hasUserReacted, props.isContextMenu),
56+
]}
57+
onPress={props.onPress}
58+
onLongPress={props.onReactionListOpen}
59+
onSecondaryInteraction={props.onReactionListOpen}
60+
ref={props.forwardedRef}
61+
enableLongPressWithHover={props.isSmallScreenWidth}
6562

66-
// Prevent text input blur when emoji reaction is clicked
67-
onMouseDown={e => e.preventDefault()}
63+
// Prevent text input blur when emoji reaction is clicked
64+
onMouseDown={e => e.preventDefault()}
65+
>
66+
<Text style={[
67+
styles.emojiReactionBubbleText,
68+
styles.userSelectNone,
69+
StyleUtils.getEmojiReactionBubbleTextStyle(props.isContextMenu),
70+
]}
6871
>
69-
<Text style={[
70-
styles.emojiReactionBubbleText,
71-
styles.userSelectNone,
72-
StyleUtils.getEmojiReactionBubbleTextStyle(props.isContextMenu),
73-
]}
74-
>
75-
{props.emojiCodes.join('')}
76-
</Text>
77-
{props.count > 0 && (
72+
{props.emojiCodes.join('')}
73+
</Text>
74+
{props.count > 0 && (
7875
<Text style={[
7976
styles.reactionCounterText,
8077
styles.userSelectNone,
81-
StyleUtils.getEmojiReactionCounterTextStyle(hasUserReacted),
78+
StyleUtils.getEmojiReactionCounterTextStyle(props.hasUserReacted),
8279
]}
8380
>
8481
{props.count}
8582
</Text>
86-
)}
87-
</Pressable>
88-
);
89-
};
83+
)}
84+
</PressableWithSecondaryInteraction>
85+
);
9086

9187
EmojiReactionBubble.propTypes = propTypes;
9288
EmojiReactionBubble.defaultProps = defaultProps;
9389
EmojiReactionBubble.displayName = 'EmojiReactionBubble';
9490

95-
export default withCurrentUserPersonalDetails(EmojiReactionBubble);
91+
export default withWindowDimensions(React.forwardRef((props, ref) => (
92+
/* eslint-disable-next-line react/jsx-props-no-spreading */
93+
<EmojiReactionBubble {...props} forwardedRef={ref} />
94+
)));

src/components/Reactions/ReportActionItemReactions.js

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
import React from 'react';
1+
import React, {useRef} from 'react';
22
import _ from 'underscore';
33
import {View} from 'react-native';
44
import PropTypes from 'prop-types';
55
import styles from '../../styles/styles';
66
import EmojiReactionBubble from './EmojiReactionBubble';
77
import emojis from '../../../assets/emojis';
88
import AddReactionBubble from './AddReactionBubble';
9+
import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../withCurrentUserPersonalDetails';
910
import getPreferredEmojiCode from './getPreferredEmojiCode';
11+
import * as PersonalDetailsUtils from '../../libs/PersonalDetailsUtils';
12+
import * as Report from '../../libs/actions/Report';
13+
import * as ReactionList from '../../pages/home/report/ReactionList/ReactionList';
1014
import Tooltip from '../Tooltip';
1115
import ReactionTooltipContent from './ReactionTooltipContent';
1216

@@ -52,22 +56,46 @@ const propTypes = {
5256
* hence this function asks to toggle the reaction by emoji.
5357
*/
5458
toggleReaction: PropTypes.func.isRequired,
59+
60+
...withCurrentUserPersonalDetailsPropTypes,
61+
};
62+
63+
const defaultProps = {
64+
...withCurrentUserPersonalDetailsDefaultProps,
5565
};
5666

5767
const ReportActionItemReactions = (props) => {
68+
const popoverReactionListAnchor = useRef(null);
5869
const reactionsWithCount = _.filter(props.reactions, reaction => reaction.users.length > 0);
5970

6071
return (
61-
<View style={[styles.flexRow, styles.flexWrap, styles.gap1, styles.mt2]}>
72+
<View
73+
ref={popoverReactionListAnchor}
74+
style={[styles.flexRow, styles.flexWrap, styles.gap1, styles.mt2]}
75+
>
76+
6277
{_.map(reactionsWithCount, (reaction) => {
6378
const reactionCount = reaction.users.length;
6479
const reactionUsers = _.map(reaction.users, sender => sender.accountID.toString());
6580
const emoji = _.find(emojis, e => e.name === reaction.emoji);
6681
const emojiCodes = getUniqueEmojiCodes(emoji, reaction.users);
82+
const hasUserReacted = Report.hasAccountIDReacted(props.currentUserPersonalDetails.accountID, reactionUsers);
6783

6884
const onPress = () => {
6985
props.toggleReaction(emoji);
7086
};
87+
const onReactionListOpen = (event) => {
88+
const users = PersonalDetailsUtils.getPersonalDetailsByIDs(reactionUsers);
89+
ReactionList.showReactionList(
90+
event,
91+
popoverReactionListAnchor.current,
92+
users,
93+
reaction.emoji,
94+
emojiCodes,
95+
reactionCount,
96+
hasUserReacted,
97+
);
98+
};
7199

72100
return (
73101
<Tooltip
@@ -81,12 +109,16 @@ const ReportActionItemReactions = (props) => {
81109
key={reaction.emoji}
82110
>
83111
<EmojiReactionBubble
112+
ref={props.forwardedRef}
84113
count={reactionCount}
85114
emojiCodes={emojiCodes}
86115
onPress={onPress}
87116
reactionUsers={reactionUsers}
117+
hasUserReacted={hasUserReacted}
118+
onReactionListOpen={onReactionListOpen}
88119
/>
89120
</Tooltip>
121+
90122
);
91123
})}
92124
{reactionsWithCount.length > 0 && <AddReactionBubble onSelectEmoji={props.toggleReaction} />}
@@ -96,4 +128,6 @@ const ReportActionItemReactions = (props) => {
96128

97129
ReportActionItemReactions.displayName = 'ReportActionItemReactions';
98130
ReportActionItemReactions.propTypes = propTypes;
99-
export default ReportActionItemReactions;
131+
ReportActionItemReactions.defaultProps = defaultProps;
132+
export default withCurrentUserPersonalDetails(ReportActionItemReactions);
133+
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/* eslint-disable rulesdir/onyx-props-must-have-default */
2+
import React from 'react';
3+
import {FlatList} from 'react-native';
4+
import PropTypes from 'prop-types';
5+
import Str from 'expensify-common/lib/str';
6+
import styles from '../../../../styles/styles';
7+
import HeaderReactionList from './HeaderReactionList';
8+
import * as ReportUtils from '../../../../libs/ReportUtils';
9+
import CONST from '../../../../CONST';
10+
import participantPropTypes from '../../../../components/participantPropTypes';
11+
import reactionPropTypes from './reactionPropTypes';
12+
import OptionRow from '../../../../components/OptionRow';
13+
import variables from '../../../../styles/variables';
14+
import withWindowDimensions from '../../../../components/withWindowDimensions';
15+
16+
const propTypes = {
17+
18+
/**
19+
* Array of personal detail objects
20+
*/
21+
users: PropTypes.arrayOf(participantPropTypes).isRequired,
22+
23+
/**
24+
* Returns true if the current account has reacted to the report action (with the given skin tone).
25+
*/
26+
hasUserReacted: PropTypes.bool,
27+
28+
...reactionPropTypes,
29+
};
30+
31+
const defaultProps = {
32+
hasUserReacted: false,
33+
};
34+
35+
/**
36+
* Given an emoji item object, render a component based on its type.
37+
* Items with the code "SPACER" return nothing and are used to fill rows up to 8
38+
* so that the sticky headers function properly
39+
*
40+
* @param {Object} params
41+
* @param {Object} params.item
42+
* @return {React.Component}
43+
*/
44+
const renderItem = ({item}) => (
45+
<OptionRow
46+
item={item}
47+
boldStyle
48+
isDisabled
49+
style={{maxWidth: variables.mobileResponsiveWidthBreakpoint}}
50+
option={{
51+
text: Str.removeSMSDomain(item.displayName),
52+
alternateText: Str.removeSMSDomain(item.login),
53+
participantsList: [item],
54+
icons: [{
55+
source: ReportUtils.getAvatar(item.avatar, item.login),
56+
name: item.login,
57+
type: CONST.ICON_TYPE_AVATAR,
58+
}],
59+
keyForList: item.login,
60+
}}
61+
/>
62+
);
63+
64+
/**
65+
* Create a unique key for each action in the FlatList.
66+
* @param {Object} item
67+
* @param {Number} index
68+
* @return {String}
69+
*/
70+
const keyExtractor = (item, index) => `${item.login}+${index}`;
71+
72+
/**
73+
* This function will be used with FlatList getItemLayout property for optimization purpose that allows skipping
74+
* the measurement of dynamic content if we know the size (height or width) of items ahead of time.
75+
* Generate and return an object with properties length(height of each individual row),
76+
* offset(distance of the current row from the top of the FlatList), index(current row index)
77+
*
78+
* @param {*} _ FlatList item
79+
* @param {Number} index row index
80+
* @returns {Object}
81+
*/
82+
const getItemLayout = (_, index) => ({
83+
index,
84+
length: variables.listItemHeightNormal,
85+
offset: variables.listItemHeightNormal * index,
86+
});
87+
88+
const BaseReactionList = (props) => {
89+
if (!props.isVisible) {
90+
return null;
91+
}
92+
return (
93+
<>
94+
<HeaderReactionList
95+
onClose={props.onClose}
96+
emojiName={props.emojiName}
97+
emojiCodes={props.emojiCodes}
98+
emojiCount={props.emojiCount}
99+
hasUserReacted={props.hasUserReacted}
100+
/>
101+
<FlatList
102+
data={props.users}
103+
renderItem={renderItem}
104+
keyExtractor={keyExtractor}
105+
getItemLayout={getItemLayout}
106+
contentContainerStyle={styles.pv2}
107+
style={[styles.reactionListContainer, !props.isSmallScreenWidth && styles.reactionListContainerFixedWidth]}
108+
/>
109+
</>
110+
);
111+
};
112+
113+
BaseReactionList.propTypes = propTypes;
114+
BaseReactionList.defaultProps = defaultProps;
115+
BaseReactionList.displayName = 'BaseReactionList';
116+
117+
export default withWindowDimensions(BaseReactionList);

0 commit comments

Comments
 (0)