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 14 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
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};
80 changes: 40 additions & 40 deletions src/components/Reactions/EmojiReactionBubble.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
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 Tooltip from '../Tooltip';
import ReactionTooltipContent from './ReactionTooltipContent';
import PressableWithSecondaryInteraction from '../PressableWithSecondaryInteraction';
import withWindowDimensions from '../withWindowDimensions';

const propTypes = {
emojiName: PropTypes.string.isRequired,
Expand Down Expand Up @@ -60,49 +57,52 @@ const defaultProps = {
...withCurrentUserPersonalDetailsDefaultProps,
};

const EmojiReactionBubble = (props) => {
const hasUserReacted = Report.hasAccountIDReacted(props.currentUserPersonalDetails.accountID, props.reactionUsers);
return (
<Tooltip
renderTooltipContent={() => (
<ReactionTooltipContent
emojiName={props.emojiName}
emojiCodes={props.emojiCodes}
accountIDs={props.reactionUsers}
/>
)}
const EmojiReactionBubble = props => (
<Tooltip
renderTooltipContent={() => (
<ReactionTooltipContent
emojiName={props.emojiName}
emojiCodes={props.emojiCodes}
accountIDs={props.reactionUsers}
/>
)}
>
<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.forwardedRef}
isLongPressEnabledWithHover={props.isSmallScreenWidth}
>
<Pressable
style={({hovered, pressed}) => [
styles.emojiReactionBubble,
StyleUtils.getEmojiReactionBubbleStyle(hovered || pressed, hasUserReacted, props.sizeScale),
]}
onPress={props.onPress}
onLongPress={props.onReactionListOpen}
<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>
</Tooltip>
);
};

)}
</PressableWithSecondaryInteraction>
</Tooltip>
);
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} />
)));
32 changes: 31 additions & 1 deletion src/components/Reactions/ReportActionItemReactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ 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';

/**
* Given an emoji object and a list of senders it will return an
Expand All @@ -28,6 +30,10 @@ const getUniqueEmojiCodes = (emoji, users) => {
return emojiCodes;
};

const defaultProps = {
...withCurrentUserPersonalDetailsDefaultProps,
};

const propTypes = {
/**
* An array of objects containing the reaction data.
Expand All @@ -50,6 +56,18 @@ 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 ReportActionItemReactions = (props) => {
Expand All @@ -62,19 +80,26 @@ 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 (
<EmojiReactionBubble
ref={props.forwardedRef}
key={reaction.emoji}
count={reactionCount}
emojiName={reaction.emoji}
emojiCodes={emojiCodes}
onPress={onPress}
reactionUsers={reactionUsers}
hasUserReacted={hasUserReacted}
onReactionListOpen={onReactionListOpen}
/>
);
})}
Expand All @@ -85,4 +110,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} />
)));

42 changes: 42 additions & 0 deletions src/pages/home/report/ReactionList/BaseReactionList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import {View, FlatList} from 'react-native';
import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize';
import ReactionListItem from './ReactionListItem';
import styles from '../../../../styles/styles';
import HeaderReactionList from './HeaderReactionList';

const propTypes = {
...withLocalizePropTypes,
};

const defaultProps = {
contentRef: null,
};

const renderItem = ({item}) => <ReactionListItem item={item} />;
const keyExtractor = item => `${item.accountID}`;

const BaseReactionList = props => (props.isVisible) && (
<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}
style={[styles.pb3, styles.pt2]}
/>
</View>
);

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

export default withLocalize(BaseReactionList);
53 changes: 53 additions & 0 deletions src/pages/home/report/ReactionList/HeaderReactionList/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import {View} 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 * as StyleUtils from '../../../../../styles/StyleUtils';

const propTypes = {
/** Selected emoji */
/** The emoji codes */
emojiCodes: PropTypes.arrayOf(PropTypes.string).isRequired,

/** The name of the emoji */
emojiName: PropTypes.string.isRequired,

/** Count of the emoji */
emojiCount: PropTypes.number.isRequired,

/**
* 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,

...withLocalizePropTypes,
};

const defaultProps = {
sizeScale: 1,
};

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 withLocalize(HeaderReactionList);

Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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';

const propTypes = {
/** Children view component for this action item */
onClose: PropTypes.func.isRequired,

/** The emoji codes */
emojiCodes: PropTypes.arrayOf(PropTypes.string).isRequired,

/** The name of the emoji */
emojiName: PropTypes.string.isRequired,

/** Count of the emoji */
emojiCount: PropTypes.number.isRequired,

/**
* 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,

...withLocalizePropTypes,
};

const defaultProps = {
sizeScale: 1,
};

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