Skip to content

Commit e87f2eb

Browse files
authored
Merge pull request #17996 from Expensify/tgolen-migrate-reactions
Support new emojiReaction data format
2 parents cc5526b + 73ca137 commit e87f2eb

File tree

13 files changed

+325
-271
lines changed

13 files changed

+325
-271
lines changed

src/CONST.js

+1
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ const CONST = {
174174
},
175175
DATE: {
176176
MOMENT_FORMAT_STRING: 'YYYY-MM-DD',
177+
SQL_DATE_TIME: 'YYYY-MM-DD HH:mm:ss',
177178
UNIX_EPOCH: '1970-01-01 00:00:00.000',
178179
MAX_DATE: '9999-12-31',
179180
MIN_DATE: '0001-01-01',

src/components/AttachmentCarousel/index.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,11 @@ class AttachmentCarousel extends React.Component {
270270

271271
/**
272272
* Defines how a single attachment should be rendered
273-
* @param {{ isAuthTokenRequired: Boolean, source: String, file: { name: String } }} item
273+
* @param {Object} item
274+
* @param {Boolean} item.isAuthTokenRequired
275+
* @param {String} item.source
276+
* @param {Object} item.file
277+
* @param {String} item.file.name
274278
* @returns {JSX.Element}
275279
*/
276280
renderItem({item}) {

src/components/AttachmentPicker/index.native.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,8 @@ class AttachmentPicker extends Component {
315315
/**
316316
* Setup native attachment selection to start after this popover closes
317317
*
318-
* @param {{pickAttachment: function}} item - an item from this.menuItemData
318+
* @param {Object} item - an item from this.menuItemData
319+
* @param {Function} item.pickAttachment
319320
*/
320321
selectItem(item) {
321322
/* setTimeout delays execution to the frame after the modal closes
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import PropTypes from 'prop-types';
2+
3+
/** All the emoji reactions for the report action. An object that looks like this:
4+
"emojiReactions": {
5+
"+1": { // The emoji added to the action
6+
"createdAt": "2021-01-01 00:00:00",
7+
"users": {
8+
2352342: { // The accountID of the user who added this emoji
9+
"skinTones": {
10+
"1": "2021-01-01 00:00:00",
11+
"2": "2021-01-01 00:00:00",
12+
},
13+
},
14+
},
15+
},
16+
},
17+
*/
18+
export default PropTypes.objectOf(
19+
PropTypes.shape({
20+
/** The time the emoji was added */
21+
createdAt: PropTypes.string,
22+
23+
/** All the users who have added this emoji */
24+
users: PropTypes.objectOf(
25+
PropTypes.shape({
26+
/** The skin tone which was used and also the timestamp of when it was added */
27+
skinTones: PropTypes.objectOf(PropTypes.string),
28+
}),
29+
),
30+
}),
31+
);

src/components/Reactions/MiniQuickEmojiReactions.js

+10-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import Icon from '../Icon';
1212
import * as Expensicons from '../Icon/Expensicons';
1313
import getButtonState from '../../libs/getButtonState';
1414
import * as EmojiPickerAction from '../../libs/actions/EmojiPickerAction';
15-
import {baseQuickEmojiReactionsPropTypes} from './QuickEmojiReactions/BaseQuickEmojiReactions';
15+
import {baseQuickEmojiReactionsPropTypes, baseQuickEmojiReactionsDefaultProps} from './QuickEmojiReactions/BaseQuickEmojiReactions';
1616
import withLocalize, {withLocalizePropTypes} from '../withLocalize';
1717
import compose from '../../libs/compose';
1818
import ONYXKEYS from '../../ONYXKEYS';
@@ -40,6 +40,7 @@ const propTypes = {
4040
};
4141

4242
const defaultProps = {
43+
...baseQuickEmojiReactionsDefaultProps,
4344
onEmojiPickerClosed: () => {},
4445
preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE,
4546
reportAction: {},
@@ -61,7 +62,7 @@ function MiniQuickEmojiReactions(props) {
6162
EmojiPickerAction.showEmojiPicker(
6263
props.onEmojiPickerClosed,
6364
(emojiCode, emojiObject) => {
64-
props.onEmojiSelected(emojiObject);
65+
props.onEmojiSelected(emojiObject, props.emojiReactions);
6566
},
6667
ref.current,
6768
undefined,
@@ -77,7 +78,7 @@ function MiniQuickEmojiReactions(props) {
7778
key={emoji.name}
7879
isDelayButtonStateComplete={false}
7980
tooltipText={`:${EmojiUtils.getLocalizedEmojiName(emoji.name, props.preferredLocale)}:`}
80-
onPress={Session.checkIfActionIsAllowed(() => props.onEmojiSelected(emoji))}
81+
onPress={Session.checkIfActionIsAllowed(() => props.onEmojiSelected(emoji, props.emojiReactions))}
8182
>
8283
<Text style={[styles.miniQuickEmojiReactionText, styles.userSelectNone]}>{EmojiUtils.getPreferredEmojiCode(emoji, props.preferredSkinTone)}</Text>
8384
</BaseMiniContextMenuItem>
@@ -105,9 +106,15 @@ MiniQuickEmojiReactions.propTypes = propTypes;
105106
MiniQuickEmojiReactions.defaultProps = defaultProps;
106107
export default compose(
107108
withLocalize,
109+
// ESLint throws an error because it can't see that emojiReactions is defined in props. It is defined in props, but
110+
// because of a couple spread operators, I think that's why ESLint struggles to see it
111+
// eslint-disable-next-line rulesdir/onyx-props-must-have-default
108112
withOnyx({
109113
preferredSkinTone: {
110114
key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE,
111115
},
116+
emojiReactions: {
117+
key: ({reportActionID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`,
118+
},
112119
}),
113120
)(MiniQuickEmojiReactions);

src/components/Reactions/QuickEmojiReactions/BaseQuickEmojiReactions.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ import styles from '../../../styles/styles';
1010
import ONYXKEYS from '../../../ONYXKEYS';
1111
import Tooltip from '../../Tooltip';
1212
import * as EmojiUtils from '../../../libs/EmojiUtils';
13+
import EmojiReactionsPropTypes from '../EmojiReactionsPropTypes';
1314
import * as Session from '../../../libs/actions/Session';
1415

1516
const baseQuickEmojiReactionsPropTypes = {
17+
emojiReactions: EmojiReactionsPropTypes,
18+
1619
/**
1720
* Callback to fire when an emoji is selected.
1821
*/
@@ -39,6 +42,7 @@ const baseQuickEmojiReactionsPropTypes = {
3942
};
4043

4144
const baseQuickEmojiReactionsDefaultProps = {
45+
emojiReactions: {},
4246
onWillShowPicker: () => {},
4347
onPressOpenPicker: () => {},
4448
reportAction: {},
@@ -67,7 +71,7 @@ function BaseQuickEmojiReactions(props) {
6771
<EmojiReactionBubble
6872
emojiCodes={[EmojiUtils.getPreferredEmojiCode(emoji, props.preferredSkinTone)]}
6973
isContextMenu
70-
onPress={Session.checkIfActionIsAllowed(() => props.onEmojiSelected(emoji))}
74+
onPress={Session.checkIfActionIsAllowed(() => props.onEmojiSelected(emoji, props.emojiReactions))}
7175
/>
7276
</View>
7377
</Tooltip>
@@ -90,9 +94,12 @@ export default withOnyx({
9094
preferredSkinTone: {
9195
key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE,
9296
},
97+
emojiReactions: {
98+
key: ({reportActionID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`,
99+
},
93100
preferredLocale: {
94101
key: ONYXKEYS.NVP_PREFERRED_LOCALE,
95102
},
96103
})(BaseQuickEmojiReactions);
97104

98-
export {baseQuickEmojiReactionsPropTypes};
105+
export {baseQuickEmojiReactionsPropTypes, baseQuickEmojiReactionsDefaultProps};

src/components/Reactions/ReportActionItemReactions.js src/components/Reactions/ReportActionItemEmojiReactions.js

+58-31
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,14 @@ import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultPro
99
import withLocalize from '../withLocalize';
1010
import compose from '../../libs/compose';
1111
import * as Report from '../../libs/actions/Report';
12+
import EmojiReactionsPropTypes from './EmojiReactionsPropTypes';
1213
import Tooltip from '../Tooltip';
1314
import ReactionTooltipContent from './ReactionTooltipContent';
1415
import * as EmojiUtils from '../../libs/EmojiUtils';
1516
import ReportScreenContext from '../../pages/home/ReportScreenContext';
1617

1718
const propTypes = {
18-
/**
19-
* An array of objects containing the reaction data.
20-
* The shape of a reaction looks like this:
21-
*
22-
* "reactionName": {
23-
* emoji: string,
24-
* users: {
25-
* accountID: string,
26-
* skinTone: number,
27-
* }[]
28-
* }
29-
*/
30-
// eslint-disable-next-line react/forbid-prop-types
31-
reactions: PropTypes.arrayOf(PropTypes.object).isRequired,
19+
emojiReactions: EmojiReactionsPropTypes,
3220

3321
/** The ID of the reportAction. It is the string representation of the a 64-bit integer. */
3422
reportActionID: PropTypes.string.isRequired,
@@ -45,27 +33,66 @@ const propTypes = {
4533

4634
const defaultProps = {
4735
...withCurrentUserPersonalDetailsDefaultProps,
36+
emojiReactions: {},
4837
};
4938

50-
function ReportActionItemReactions(props) {
39+
function ReportActionItemEmojiReactions(props) {
5140
const {reactionListRef} = useContext(ReportScreenContext);
5241
const popoverReactionListAnchor = useRef(null);
53-
const reactionsWithCount = _.filter(props.reactions, (reaction) => reaction.users.length > 0);
42+
let totalReactionCount = 0;
43+
44+
// Each emoji is sorted by the oldest timestamp of user reactions so that they will always appear in the same order for everyone
45+
const sortedReactions = _.sortBy(props.emojiReactions, (emojiReaction, emojiName) => {
46+
// Since the emojiName is only stored as the object key, when _.sortBy() runs, the object is converted to an array and the
47+
// keys are lost. To keep from losing the emojiName, it's copied to the emojiReaction object.
48+
// eslint-disable-next-line no-param-reassign
49+
emojiReaction.emojiName = emojiName;
50+
const oldestUserReactionTimestamp = _.chain(emojiReaction.users)
51+
.reduce((allTimestampsArray, userData) => {
52+
if (!userData) {
53+
return allTimestampsArray;
54+
}
55+
_.each(userData.skinTones, (createdAt) => {
56+
allTimestampsArray.push(createdAt);
57+
});
58+
return allTimestampsArray;
59+
}, [])
60+
.sort()
61+
.first()
62+
.value();
63+
64+
// Just in case two emojis have the same timestamp, also combine the timestamp with the
65+
// emojiName so that the order will always be the same. Without this, the order can be pretty random
66+
// and shift around a little bit.
67+
return (oldestUserReactionTimestamp || emojiReaction.createdAt) + emojiName;
68+
});
5469

5570
return (
5671
<View
5772
ref={popoverReactionListAnchor}
5873
style={[styles.flexRow, styles.flexWrap, styles.gap1, styles.mt2]}
5974
>
60-
{_.map(reactionsWithCount, (reaction) => {
61-
const reactionCount = reaction.users.length;
62-
const reactionUsers = _.map(reaction.users, (sender) => sender.accountID);
63-
const emoji = EmojiUtils.findEmojiByName(reaction.emoji);
64-
const emojiCodes = EmojiUtils.getUniqueEmojiCodes(emoji, reaction.users);
65-
const hasUserReacted = Report.hasAccountIDReacted(props.currentUserPersonalDetails.accountID, reactionUsers);
75+
{_.map(sortedReactions, (reaction) => {
76+
const reactionEmojiName = reaction.emojiName;
77+
const usersWithReactions = _.pick(reaction.users, _.identity);
78+
let reactionCount = 0;
79+
80+
// Loop through the users who have reacted and see how many skintones they reacted with so that we get the total count
81+
_.forEach(usersWithReactions, (user) => {
82+
reactionCount += _.size(user.skinTones);
83+
});
84+
if (!reactionCount) {
85+
return null;
86+
}
87+
totalReactionCount += reactionCount;
88+
const emojiAsset = EmojiUtils.findEmojiByName(reactionEmojiName);
89+
const emojiCodes = EmojiUtils.getUniqueEmojiCodes(emojiAsset, reaction.users);
90+
const hasUserReacted = Report.hasAccountIDEmojiReacted(props.currentUserPersonalDetails.accountID, reaction.users);
91+
const reactionUsers = _.keys(usersWithReactions);
92+
const reactionUserAccountIDs = _.map(reactionUsers, Number);
6693

6794
const onPress = () => {
68-
props.toggleReaction(emoji);
95+
props.toggleReaction(emojiAsset);
6996
};
7097

7198
const onReactionListOpen = (event) => {
@@ -76,14 +103,14 @@ function ReportActionItemReactions(props) {
76103
<Tooltip
77104
renderTooltipContent={() => (
78105
<ReactionTooltipContent
79-
emojiName={EmojiUtils.getLocalizedEmojiName(reaction.emoji, props.preferredLocale)}
106+
emojiName={EmojiUtils.getLocalizedEmojiName(reactionEmojiName, props.preferredLocale)}
80107
emojiCodes={emojiCodes}
81-
accountIDs={reactionUsers}
108+
accountIDs={reactionUserAccountIDs}
82109
currentUserPersonalDetails={props.currentUserPersonalDetails}
83110
/>
84111
)}
85112
renderTooltipContentKey={[..._.map(reactionUsers, (user) => user.toString()), ...emojiCodes]}
86-
key={reaction.emoji}
113+
key={reactionEmojiName}
87114
>
88115
<View>
89116
<EmojiReactionBubble
@@ -99,7 +126,7 @@ function ReportActionItemReactions(props) {
99126
</Tooltip>
100127
);
101128
})}
102-
{reactionsWithCount.length > 0 && (
129+
{totalReactionCount > 0 && (
103130
<AddReactionBubble
104131
onSelectEmoji={props.toggleReaction}
105132
reportAction={{reportActionID: props.reportActionID}}
@@ -109,7 +136,7 @@ function ReportActionItemReactions(props) {
109136
);
110137
}
111138

112-
ReportActionItemReactions.displayName = 'ReportActionItemReactions';
113-
ReportActionItemReactions.propTypes = propTypes;
114-
ReportActionItemReactions.defaultProps = defaultProps;
115-
export default compose(withLocalize, withCurrentUserPersonalDetails)(ReportActionItemReactions);
139+
ReportActionItemEmojiReactions.displayName = 'ReportActionItemReactions';
140+
ReportActionItemEmojiReactions.propTypes = propTypes;
141+
ReportActionItemEmojiReactions.defaultProps = defaultProps;
142+
export default compose(withLocalize, withCurrentUserPersonalDetails)(ReportActionItemEmojiReactions);

src/components/ThumbnailImage.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ class ThumbnailImage extends PureComponent {
7878
/**
7979
* Update the state with the computed thumbnail sizes.
8080
*
81-
* @param {{ width: number, height: number }} Params - width and height of the original image.
81+
* @param {Object} Params - width and height of the original image.
82+
* @param {Number} Params.width
83+
* @param {Number} Params.height
8284
*/
8385
updateImageSize({width, height}) {
8486
const {thumbnailWidth, thumbnailHeight} = this.calculateThumbnailImageSize(width, height);

src/libs/EmojiUtils.js

+15-10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import _ from 'underscore';
22
import moment from 'moment';
33
import Str from 'expensify-common/lib/str';
44
import Onyx from 'react-native-onyx';
5+
import lodashGet from 'lodash/get';
56
import ONYXKEYS from '../ONYXKEYS';
67
import CONST from '../CONST';
78
import emojisTrie from './EmojiTrie';
@@ -384,20 +385,24 @@ const getPreferredEmojiCode = (emoji, preferredSkinTone) => {
384385
* Given an emoji object and a list of senders it will return an
385386
* array of emoji codes, that represents all used variations of the
386387
* emoji.
387-
* @param {{ name: string, code: string, types: string[] }} emoji
388+
* @param {Object} emojiAsset
389+
* @param {String} emojiAsset.name
390+
* @param {String} emojiAsset.code
391+
* @param {String[]} [emojiAsset.types]
388392
* @param {Array} users
389393
* @return {string[]}
390394
* */
391-
const getUniqueEmojiCodes = (emoji, users) => {
392-
const emojiCodes = [];
393-
_.forEach(users, (user) => {
394-
const emojiCode = getPreferredEmojiCode(emoji, user.skinTone);
395-
396-
if (emojiCode && !emojiCodes.includes(emojiCode)) {
397-
emojiCodes.push(emojiCode);
398-
}
395+
const getUniqueEmojiCodes = (emojiAsset, users) => {
396+
const uniqueEmojiCodes = [];
397+
_.each(users, (userSkinTones) => {
398+
_.each(lodashGet(userSkinTones, 'skinTones'), (createdAt, skinTone) => {
399+
const emojiCode = getPreferredEmojiCode(emojiAsset, skinTone);
400+
if (emojiCode && !uniqueEmojiCodes.includes(emojiCode)) {
401+
uniqueEmojiCodes.push(emojiCode);
402+
}
403+
});
399404
});
400-
return emojiCodes;
405+
return uniqueEmojiCodes;
401406
};
402407

403408
export {

0 commit comments

Comments
 (0)