Skip to content

Detailed list of reaction senders when long-/right-pressing a reaction #15685

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

Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f6279ea
ReactionList
perunt Mar 6, 2023
bc5a517
update styles
perunt Mar 7, 2023
5732ada
add isLongPressEnabledWithHover to PressableWithSecondaryInteraction
perunt Mar 7, 2023
72d53e9
reactionCount
perunt Mar 7, 2023
0280636
typo
perunt Mar 7, 2023
82b02be
Merge branch 'main' of https://github.com/margelo/expensify-app-fork …
perunt Mar 8, 2023
156d6ef
Merge branch 'hanno/feat-tolltip-reaction-senders' of https://github.…
perunt Mar 10, 2023
c66388b
import getPersonalDetailsByIDs
perunt Mar 10, 2023
0317bd7
take hasUserReacted from Report
perunt Mar 10, 2023
e36ba93
Resolving a merge conflict
perunt Mar 10, 2023
b16febf
Merge branch 'hanno/feat-tolltip-reaction-senders' of https://github.…
perunt Mar 13, 2023
fbc69cf
Merge branch 'main' of https://github.com/margelo/expensify-app-fork …
perunt Mar 15, 2023
502920e
remove container
perunt Mar 16, 2023
5bc555d
Merge branch 'main' of https://github.com/Expensify/App into perunt/r…
perunt Mar 16, 2023
6bc54ce
update propTypes
perunt Mar 20, 2023
e96d82b
changes after the review
perunt Mar 20, 2023
acf6f2e
Merge branch 'main' of https://github.com/Expensify/App into perunt/r…
perunt Mar 20, 2023
838585d
Merge branch 'main' of https://github.com/Expensify/App into perunt/r…
perunt Mar 23, 2023
01848c6
Merge branch 'perunt/reaction-list-on-secondary-interaction' of https…
perunt Mar 23, 2023
5523614
Merge branch 'main' of https://github.com/Expensify/App into perunt/r…
perunt Mar 24, 2023
e528087
remove unnecessary check for instance ID
perunt Mar 24, 2023
b91f52c
remove space
perunt Mar 24, 2023
96d05f6
Merge branch 'main' of https://github.com/margelo/expensify-app-fork …
perunt Mar 27, 2023
524546b
removed unused eslint rules which appeared after merge
perunt Mar 27, 2023
b4343a8
add onyx eslint rules
perunt Mar 27, 2023
d129b8c
Merge branch 'main' of https://github.com/Expensify/App into perunt/r…
perunt Mar 30, 2023
6ebe8ac
update keyExtractor key
perunt Mar 30, 2023
308f673
remove mobile HeaderReactionList
perunt Mar 30, 2023
38db154
Merge branch 'main' of https://github.com/margelo/expensify-app-fork …
perunt Apr 6, 2023
b2a1c65
style
perunt Apr 6, 2023
4ffd2ff
Improvements following review
perunt Apr 13, 2023
6df949e
Merge branch 'main' of https://github.com/Expensify/App into perunt/r…
perunt Apr 13, 2023
907fd16
removed sizeScale
perunt Apr 13, 2023
a09ad6b
revert, rename enableLongPressWithHover
perunt Apr 28, 2023
cb14807
clean
perunt Apr 28, 2023
5dada1a
Merge branch 'main' of https://github.com/Expensify/App into perunt/r…
perunt Apr 28, 2023
a989192
popoverWidth
perunt Apr 28, 2023
3604015
disable touch response for item list
perunt May 1, 2023
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
1 change: 1 addition & 0 deletions src/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,7 @@ const CONST = {
},
NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT: 256,
EMOJI_PICKER_ITEM_HEIGHT: 32,
REACTION_LIST_ITEM_HEIGHT: 64,
EMOJI_PICKER_HEADER_HEIGHT: 32,
RECIPIENT_LOCAL_TIME_HEIGHT: 25,
EMOJI_SUGGESTER: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class PressableWithSecondaryInteraction extends Component {
style={this.props.inline && styles.dInline}
onPressIn={this.props.onPressIn}
onLongPress={(e) => {
if (DeviceCapabilities.hasHoverSupport()) {
if (DeviceCapabilities.hasHoverSupport() && !this.props.isLongPressEnabledWithHover) {
return;
}
if (this.props.withoutFocusOnSecondaryInteraction && this.pressableRef) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const defaultProps = {
preventDefaultContentMenu: true,
inline: false,
withoutFocusOnSecondaryInteraction: false,
isLongPressEnabledWithHover: false,
Copy link
Contributor

@Julesssss Julesssss Mar 27, 2023

Choose a reason for hiding this comment

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

NAB: Is ...withHover necessary here? It sounds a bit confusing to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since I've extended the properties of this component, I think it's reasonable to define a default value for this optional property. This would make it more predictable for other developers who use this component

Copy link
Contributor

Choose a reason for hiding this comment

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

More specifically, I meant that this prop name is quite long and didn't make sense to me immediately.

};

export {propTypes, defaultProps};
75 changes: 39 additions & 36 deletions src/components/Reactions/EmojiReactionBubble.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
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 {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../withCurrentUserPersonalDetails';
import PressableWithSecondaryInteraction from '../PressableWithSecondaryInteraction';
import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions';

const propTypes = {
/**
Expand All @@ -32,63 +30,68 @@ const propTypes = {
*/
count: PropTypes.number,

/**
* The account ids of the users who reacted.
*/
reactionUsers: PropTypes.arrayOf(PropTypes.string),

/**
* The default size of the reaction bubble is defined
* by the styles in styles.js. This scale factor can be used
* to make the bubble bigger or smaller.
*/
sizeScale: PropTypes.number,

/**
* Returns true if the current account has reacted to the report action (with the given skin tone).
*/
hasUserReacted: PropTypes.bool,

...withCurrentUserPersonalDetailsPropTypes,
...windowDimensionsPropTypes,
};

const defaultProps = {
count: 0,
onReactionListOpen: () => {},
reactionUsers: [],
sizeScale: 1,
hasUserReacted: false,

...withCurrentUserPersonalDetailsDefaultProps,
};

const EmojiReactionBubble = (props) => {
const hasUserReacted = Report.hasAccountIDReacted(props.currentUserPersonalDetails.accountID, props.reactionUsers);
return (
<Pressable
style={({hovered, pressed}) => [
styles.emojiReactionBubble,
StyleUtils.getEmojiReactionBubbleStyle(hovered || pressed, hasUserReacted, props.sizeScale),
]}
onPress={props.onPress}
onLongPress={props.onReactionListOpen}
const EmojiReactionBubble = props => (
<PressableWithSecondaryInteraction
style={({hovered, pressed}) => [
styles.emojiReactionBubble,
StyleUtils.getEmojiReactionBubbleStyle(hovered || pressed, props.hasUserReacted, props.sizeScale),
]}
onPress={props.onPress}
onLongPress={props.onReactionListOpen}
onSecondaryInteraction={props.onReactionListOpen}
ref={props.forwardRef}
isLongPressEnabledWithHover={props.isSmallScreenWidth}
>
<Text style={[
styles.emojiReactionText,
StyleUtils.getEmojiReactionTextStyle(props.sizeScale),
]}
>
<Text style={[
styles.emojiReactionText,
StyleUtils.getEmojiReactionTextStyle(props.sizeScale),
]}
>
{props.emojiCodes.join('')}
</Text>
{props.count > 0 && (
{props.emojiCodes.join('')}
</Text>
{props.count > 0 && (

<Text style={[
styles.reactionCounterText,
StyleUtils.getEmojiReactionCounterTextStyle(hasUserReacted, props.sizeScale),
StyleUtils.getEmojiReactionCounterTextStyle(props.hasUserReacted, props.sizeScale),
]}
>
{props.count}
</Text>
)}
</Pressable>
);
};
)}
</PressableWithSecondaryInteraction>
);

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 */
<EmojiReactionBubble {...props} forwardRef={ref} />
)));
34 changes: 33 additions & 1 deletion src/components/Reactions/ReportActionItemReactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 Report from '../../libs/actions/Report';

import Tooltip from '../Tooltip';
import ReactionTooltipContent from './ReactionTooltipContent';

Expand Down Expand Up @@ -52,6 +56,21 @@ const propTypes = {
* hence this function asks to toggle the reaction by emoji.
*/
toggleReaction: PropTypes.func.isRequired,

/** A ref to PressableWithSecondaryInteraction */
forwardedRef: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({current: PropTypes.instanceOf(React.Component)}),
]).isRequired,

/** Function which opens Reaction List popup */
onReactionListOpen: PropTypes.func.isRequired,

...withCurrentUserPersonalDetailsPropTypes,
};

const defaultProps = {
...withCurrentUserPersonalDetailsDefaultProps,
};

const ReportActionItemReactions = (props) => {
Expand All @@ -64,10 +83,14 @@ const ReportActionItemReactions = (props) => {
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) => {
props.onReactionListOpen(event, reactionUsers, reaction.emoji, emojiCodes, reactionCount, hasUserReacted);
};

return (
<Tooltip
Expand All @@ -80,13 +103,17 @@ const ReportActionItemReactions = (props) => {
)}
>
<EmojiReactionBubble
ref={props.forwardedRef}
Copy link
Contributor

Choose a reason for hiding this comment

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

✋ Coming from #25976

We are preventing the popover from closing when clicking on the popover and the anchor. In this case, the entire View component where the reactions sit is used as the anchor, causing clicks to be ignored in this area.

key={reaction.emoji}
count={reactionCount}
emojiCodes={emojiCodes}
onPress={onPress}
reactionUsers={reactionUsers}
hasUserReacted={hasUserReacted}
onReactionListOpen={onReactionListOpen}
/>
</Tooltip>

);
})}
{reactionsWithCount.length > 0 && <AddReactionBubble onSelectEmoji={props.toggleReaction} />}
Expand All @@ -96,4 +123,9 @@ const ReportActionItemReactions = (props) => {

ReportActionItemReactions.displayName = 'ReportActionItemReactions';
ReportActionItemReactions.propTypes = propTypes;
export default ReportActionItemReactions;
ReportActionItemReactions.defaultProps = defaultProps;
export default withCurrentUserPersonalDetails(React.forwardRef((props, ref) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<ReportActionItemReactions {...props} forwardedRef={ref} />
)));

73 changes: 73 additions & 0 deletions src/pages/home/report/ReactionList/BaseReactionList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React from 'react';
import {View, FlatList} from 'react-native';
import PropTypes from 'prop-types';
import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize';
import ReactionListItem from './ReactionListItem';
import styles from '../../../../styles/styles';
import HeaderReactionList from './HeaderReactionList';
import CONST from '../../../../CONST';
import participantPropTypes from '../../../../components/participantPropTypes';
import {
propTypes as reactionPropTypes,
defaultProps as reactionDefaultProps,
} from './HeaderReactionList/reactionPropTypes';

const propTypes = {

/**
* Array of personal detail objects
*/
users: PropTypes.arrayOf(participantPropTypes),

/**
* Returns true if the current account has reacted to the report action (with the given skin tone).
*/
hasUserReacted: PropTypes.bool,

...reactionPropTypes,
...withLocalizePropTypes,
};

const defaultProps = {
...reactionDefaultProps,
};

const renderItem = ({item}) => <ReactionListItem item={item} />;
const keyExtractor = item => `${item.accountID}`;
const getItemLayout = (_, index) => ({
index,
length: CONST.REACTION_LIST_ITEM_HEIGHT,
offset: CONST.REACTION_LIST_ITEM_HEIGHT * index,
});

// const BaseReactionList = props => (props.isVisible) && (
const BaseReactionList = (props) => {
if (!props.isVisible) {
return null;
}
return (
<View style={styles.reactionListContainer}>
<HeaderReactionList
onClose={props.onClose}
emojiName={props.emojiName}
emojiCodes={props.emojiCodes}
emojiCount={props.emojiCount}
hasUserReacted={props.hasUserReacted}
sizeScale={props.sizeScale}
/>
<FlatList
data={props.users}
renderItem={renderItem}
keyExtractor={keyExtractor}
getItemLayout={getItemLayout}
style={[styles.pb3, styles.pt2]}
/>
</View>
);
};

BaseReactionList.propTypes = propTypes;
BaseReactionList.defaultProps = defaultProps;
BaseReactionList.displayName = 'BaseReactionList';

export default withLocalize(BaseReactionList);
38 changes: 38 additions & 0 deletions src/pages/home/report/ReactionList/HeaderReactionList/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import {View} from 'react-native';
import styles from '../../../../../styles/styles';
import Text from '../../../../../components/Text';
import * as StyleUtils from '../../../../../styles/StyleUtils';
import {
propTypes as reactionPropTypes,
defaultProps as reactionDefaultProps,
} from './reactionPropTypes';

const propTypes = {
...reactionPropTypes,
};

const defaultProps = {
...reactionDefaultProps,
};

const HeaderReactionList = props => (
<View style={[styles.pt4, styles.mh5, styles.emojiReactionListHeader, styles.flexRow]}>
<View style={[styles.emojiReactionListHeaderBubble, StyleUtils.getEmojiReactionListHeaderBubbleStyle(props.hasUserReacted)]}>
<Text style={[styles.emojiReactionText, StyleUtils.getEmojiReactionTextStyle(props.sizeScale)]}>
{props.emojiCodes.join('')}
</Text>
<Text style={[styles.reactionCounterText, StyleUtils.getEmojiReactionCounterTextStyle(props.hasUserReacted, props.sizeScale)]}>
{props.emojiCount}
</Text>
</View>
<Text style={styles.reactionListHeaderText}>{`:${props.emojiName}:`}</Text>
</View>
);

HeaderReactionList.propTypes = propTypes;
HeaderReactionList.defaultProps = defaultProps;
HeaderReactionList.displayName = 'HeaderReactionList';

export default HeaderReactionList;

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';
import {View, TouchableOpacity} from 'react-native';
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 {
propTypes as reactionPropTypes,
defaultProps as reactionDefaultProps,
} from './reactionPropTypes';

const propTypes = {
...reactionPropTypes,
...withLocalizePropTypes,
};

const defaultProps = {
...reactionDefaultProps,
};

const HeaderReactionList = props => (
<View style={[styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, styles.emojiReactionListHeader]}>
<View style={styles.flexRow}>
<View style={[styles.emojiReactionListHeaderBubble, StyleUtils.getEmojiReactionListHeaderBubbleStyle(props.hasUserReacted)]}>
<Text style={[styles.emojiReactionText, StyleUtils.getEmojiReactionTextStyle(props.sizeScale)]}>
{props.emojiCodes.join('')}
</Text>
<Text style={[styles.reactionCounterText, StyleUtils.getEmojiReactionCounterTextStyle(props.hasUserReacted, props.sizeScale)]}>
{props.emojiCount}
</Text>
</View>
<Text style={styles.reactionListHeaderText}>{`:${props.emojiName}:`}</Text>
</View>

<TouchableOpacity
onPress={props.onClose}
accessibilityRole="button"
accessibilityLabel={props.translate('common.close')}
>
<Icon src={Expensicons.Close} />
</TouchableOpacity>
</View>
);

HeaderReactionList.propTypes = propTypes;
HeaderReactionList.defaultProps = defaultProps;
HeaderReactionList.displayName = 'HeaderReactionList';

export default withLocalize(HeaderReactionList);

Loading