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 all 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 @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const defaultProps = {
preventDefaultContextMenu: true,
inline: false,
withoutFocusOnSecondaryInteraction: false,
enableLongPressWithHover: false,
};

export {propTypes, defaultProps};
79 changes: 39 additions & 40 deletions src/components/Reactions/EmojiReactionBubble.js
Original file line number Diff line number Diff line change
@@ -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 = {
/**
Expand All @@ -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 (
<Pressable
style={({hovered, pressed}) => [
styles.emojiReactionBubble,
StyleUtils.getEmojiReactionBubbleStyle(hovered || pressed, hasUserReacted, props.isContextMenu),
]}
onPress={props.onPress}
onLongPress={props.onReactionListOpen}
const EmojiReactionBubble = props => (
<PressableWithSecondaryInteraction
style={({hovered, pressed}) => [
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()}
>
<Text style={[
styles.emojiReactionBubbleText,
styles.userSelectNone,
StyleUtils.getEmojiReactionBubbleTextStyle(props.isContextMenu),
]}
>
<Text style={[
styles.emojiReactionBubbleText,
styles.userSelectNone,
StyleUtils.getEmojiReactionBubbleTextStyle(props.isContextMenu),
]}
>
{props.emojiCodes.join('')}
</Text>
{props.count > 0 && (
{props.emojiCodes.join('')}
</Text>
{props.count > 0 && (
<Text style={[
styles.reactionCounterText,
styles.userSelectNone,
StyleUtils.getEmojiReactionCounterTextStyle(hasUserReacted),
StyleUtils.getEmojiReactionCounterTextStyle(props.hasUserReacted),
]}
>
{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} forwardedRef={ref} />
)));
40 changes: 37 additions & 3 deletions src/components/Reactions/ReportActionItemReactions.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import React from 'react';
import React, {useRef} from 'react';
import _ from 'underscore';
import {View} from 'react-native';
import PropTypes from 'prop-types';
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';

Expand Down Expand Up @@ -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 (
<View style={[styles.flexRow, styles.flexWrap, styles.gap1, styles.mt2]}>
<View
ref={popoverReactionListAnchor}
style={[styles.flexRow, styles.flexWrap, styles.gap1, styles.mt2]}
>

{_.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 (
<Tooltip
Expand All @@ -81,12 +109,16 @@ const ReportActionItemReactions = (props) => {
key={reaction.emoji}
>
<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.

count={reactionCount}
emojiCodes={emojiCodes}
onPress={onPress}
reactionUsers={reactionUsers}
hasUserReacted={hasUserReacted}
onReactionListOpen={onReactionListOpen}
/>
</Tooltip>

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

ReportActionItemReactions.displayName = 'ReportActionItemReactions';
ReportActionItemReactions.propTypes = propTypes;
export default ReportActionItemReactions;
ReportActionItemReactions.defaultProps = defaultProps;
export default withCurrentUserPersonalDetails(ReportActionItemReactions);

117 changes: 117 additions & 0 deletions src/pages/home/report/ReactionList/BaseReactionList.js
Original file line number Diff line number Diff line change
@@ -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}) => (
<OptionRow
item={item}
boldStyle
isDisabled
Copy link
Member

Choose a reason for hiding this comment

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

This also makes the text on the item unselectable. Is it fine? @Julesssss and others.

Copy link
Contributor

Choose a reason for hiding this comment

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

Good spot. I think that's okay given that there are other ways for web/Desktop users to copy this text.

style={{maxWidth: variables.mobileResponsiveWidthBreakpoint}}
option={{
text: Str.removeSMSDomain(item.displayName),
alternateText: Str.removeSMSDomain(item.login),
participantsList: [item],
icons: [{
source: ReportUtils.getAvatar(item.avatar, item.login),
name: item.login,
type: CONST.ICON_TYPE_AVATAR,
}],
keyForList: item.login,
}}
/>
);

/**
* 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 (
<>
<HeaderReactionList
onClose={props.onClose}
emojiName={props.emojiName}
emojiCodes={props.emojiCodes}
emojiCount={props.emojiCount}
hasUserReacted={props.hasUserReacted}
/>
<FlatList
data={props.users}
renderItem={renderItem}
keyExtractor={keyExtractor}
getItemLayout={getItemLayout}
contentContainerStyle={styles.pv2}
style={[styles.reactionListContainer, !props.isSmallScreenWidth && styles.reactionListContainerFixedWidth]}
/>
</>
);
};

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

export default withWindowDimensions(BaseReactionList);
Loading