diff --git a/src/components/Reactions/ReportActionItemEmojiReactions.js b/src/components/Reactions/ReportActionItemEmojiReactions.js
index 7ead2ab67ae7..5fdf74f877dd 100644
--- a/src/components/Reactions/ReportActionItemEmojiReactions.js
+++ b/src/components/Reactions/ReportActionItemEmojiReactions.js
@@ -9,7 +9,6 @@ import AddReactionBubble from './AddReactionBubble';
import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../withCurrentUserPersonalDetails';
import withLocalize from '../withLocalize';
import compose from '../../libs/compose';
-import * as Report from '../../libs/actions/Report';
import EmojiReactionsPropTypes from './EmojiReactionsPropTypes';
import Tooltip from '../Tooltip';
import ReactionTooltipContent from './ReactionTooltipContent';
@@ -52,71 +51,42 @@ function ReportActionItemEmojiReactions(props) {
const reportAction = props.reportAction;
const reportActionID = reportAction.reportActionID;
- // Each emoji is sorted by the oldest timestamp of user reactions so that they will always appear in the same order for everyone
- const sortedReactions = _.sortBy(props.emojiReactions, (emojiReaction, emojiName) => {
- // Since the emojiName is only stored as the object key, when _.sortBy() runs, the object is converted to an array and the
- // keys are lost. To keep from losing the emojiName, it's copied to the emojiReaction object.
- // eslint-disable-next-line no-param-reassign
- emojiReaction.emojiName = emojiName;
- const oldestUserReactionTimestamp = _.chain(emojiReaction.users)
- .reduce((allTimestampsArray, userData) => {
- if (!userData) {
- return allTimestampsArray;
- }
- _.each(userData.skinTones, (createdAt) => {
- allTimestampsArray.push(createdAt);
- });
- return allTimestampsArray;
- }, [])
- .sort()
- .first()
- .value();
-
- // Just in case two emojis have the same timestamp, also combine the timestamp with the
- // emojiName so that the order will always be the same. Without this, the order can be pretty random
- // and shift around a little bit.
- return (oldestUserReactionTimestamp || emojiReaction.createdAt) + emojiName;
- });
-
- const formattedReactions = _.map(sortedReactions, (reaction) => {
- const reactionEmojiName = reaction.emojiName;
- const usersWithReactions = _.pick(reaction.users, _.identity);
- let reactionCount = 0;
-
- // Loop through the users who have reacted and see how many skintones they reacted with so that we get the total count
- _.forEach(usersWithReactions, (user) => {
- reactionCount += _.size(user.skinTones);
- });
- if (!reactionCount) {
- return null;
- }
- totalReactionCount += reactionCount;
- const emojiAsset = EmojiUtils.findEmojiByName(reactionEmojiName);
- const emojiCodes = EmojiUtils.getUniqueEmojiCodes(emojiAsset, reaction.users);
- const hasUserReacted = Report.hasAccountIDEmojiReacted(props.currentUserPersonalDetails.accountID, reaction.users);
- const reactionUsers = _.keys(usersWithReactions);
- const reactionUserAccountIDs = _.map(reactionUsers, Number);
-
- const onPress = () => {
- props.toggleReaction(emojiAsset);
- };
-
- const onReactionListOpen = (event) => {
- reactionListRef.current.showReactionList(event, popoverReactionListAnchors.current[reactionEmojiName], reactionEmojiName, reportActionID);
- };
-
- return {
- reactionEmojiName,
- emojiCodes,
- reactionUserAccountIDs,
- onPress,
- reactionUsers,
- reactionCount,
- hasUserReacted,
- onReactionListOpen,
- pendingAction: reaction.pendingAction,
- };
- });
+ const formattedReactions = _.chain(props.emojiReactions)
+ .map((emojiReaction, emojiName) => {
+ const {emoji, emojiCodes, reactionCount, hasUserReacted, userAccountIDs, oldestTimestamp} = EmojiUtils.getEmojiReactionDetails(
+ emojiName,
+ emojiReaction,
+ props.currentUserPersonalDetails.accountID,
+ );
+
+ if (reactionCount === 0) {
+ return null;
+ }
+ totalReactionCount += reactionCount;
+
+ const onPress = () => {
+ props.toggleReaction(emoji);
+ };
+
+ const onReactionListOpen = (event) => {
+ reactionListRef.current.showReactionList(event, popoverReactionListAnchors.current[emojiName], emojiName, reportActionID);
+ };
+
+ return {
+ emojiCodes,
+ userAccountIDs,
+ reactionCount,
+ hasUserReacted,
+ oldestTimestamp,
+ onPress,
+ onReactionListOpen,
+ reactionEmojiName: emojiName,
+ pendingAction: emojiReaction.pendingAction,
+ };
+ })
+ // Each emoji is sorted by the oldest timestamp of user reactions so that they will always appear in the same order for everyone
+ .sortBy('oldestTimestamp')
+ .value();
return (
totalReactionCount > 0 && (
@@ -131,11 +101,11 @@ function ReportActionItemEmojiReactions(props) {
)}
- renderTooltipContentKey={[..._.map(reaction.reactionUsers, (user) => user.toString()), ...reaction.emojiCodes]}
+ renderTooltipContentKey={[..._.map(reaction.userAccountIDs, String), ...reaction.emojiCodes]}
key={reaction.reactionEmojiName}
>
@@ -148,7 +118,6 @@ function ReportActionItemEmojiReactions(props) {
count={reaction.reactionCount}
emojiCodes={reaction.emojiCodes}
onPress={reaction.onPress}
- reactionUsers={reaction.reactionUsers}
hasUserReacted={reaction.hasUserReacted}
onReactionListOpen={reaction.onReactionListOpen}
shouldBlockReactions={props.shouldBlockReactions}
diff --git a/src/libs/EmojiUtils.js b/src/libs/EmojiUtils.js
index 344d0c3bd397..05ad1bd3c2ce 100644
--- a/src/libs/EmojiUtils.js
+++ b/src/libs/EmojiUtils.js
@@ -3,6 +3,8 @@ import {getUnixTime} from 'date-fns';
import Str from 'expensify-common/lib/str';
import Onyx from 'react-native-onyx';
import lodashGet from 'lodash/get';
+import lodashMin from 'lodash/min';
+import lodashSum from 'lodash/sum';
import ONYXKEYS from '../ONYXKEYS';
import CONST from '../CONST';
import emojisTrie from './EmojiTrie';
@@ -80,7 +82,7 @@ const getEmojiUnicode = _.memoize((input) => {
const pairs = [];
- // Some Emojis in UTF-16 are stored as pair of 2 Unicode characters (eg Flags)
+ // Some Emojis in UTF-16 are stored as a pair of 2 Unicode characters (e.g. Flags)
// The first char is generally between the range U+D800 to U+DBFF called High surrogate
// & the second char between the range U+DC00 to U+DFFF called low surrogate
// More info in the following links:
@@ -474,7 +476,7 @@ const getPreferredEmojiCode = (emoji, preferredSkinTone) => {
/**
* Given an emoji object and a list of senders it will return an
* array of emoji codes, that represents all used variations of the
- * emoji.
+ * emoji, sorted by the reaction timestamp.
* @param {Object} emojiAsset
* @param {String} emojiAsset.name
* @param {String} emojiAsset.code
@@ -483,16 +485,110 @@ const getPreferredEmojiCode = (emoji, preferredSkinTone) => {
* @return {string[]}
* */
const getUniqueEmojiCodes = (emojiAsset, users) => {
- const uniqueEmojiCodes = [];
- _.each(users, (userSkinTones) => {
- _.each(lodashGet(userSkinTones, 'skinTones'), (createdAt, skinTone) => {
- const emojiCode = getPreferredEmojiCode(emojiAsset, skinTone);
- if (emojiCode && !uniqueEmojiCodes.includes(emojiCode)) {
- uniqueEmojiCodes.push(emojiCode);
+ const emojiCodes = _.reduce(
+ users,
+ (result, userSkinTones) => {
+ _.each(lodashGet(userSkinTones, 'skinTones'), (createdAt, skinTone) => {
+ const emojiCode = getPreferredEmojiCode(emojiAsset, skinTone);
+ if (!!emojiCode && (!result[emojiCode] || createdAt < result[emojiCode])) {
+ // eslint-disable-next-line no-param-reassign
+ result[emojiCode] = createdAt;
+ }
+ });
+ return result;
+ },
+ {},
+ );
+
+ return _.chain(emojiCodes)
+ .pairs()
+ .sortBy((entry) => new Date(entry[1])) // Sort by values (timestamps)
+ .map((entry) => entry[0]) // Extract keys (emoji codes)
+ .value();
+};
+
+/**
+ * Given an emoji reaction object and its name, it populates it with the oldest reaction timestamps.
+ * @param {Object} emoji
+ * @param {String} emojiName
+ * @returns {Object}
+ */
+const enrichEmojiReactionWithTimestamps = (emoji, emojiName) => {
+ let oldestEmojiTimestamp = null;
+
+ const usersWithTimestamps = _.chain(emoji.users)
+ .pick(_.identity)
+ .mapObject((user, id) => {
+ const oldestUserTimestamp = lodashMin(_.values(user.skinTones));
+
+ if (!oldestEmojiTimestamp || oldestUserTimestamp < oldestEmojiTimestamp) {
+ oldestEmojiTimestamp = oldestUserTimestamp;
}
- });
- });
- return uniqueEmojiCodes;
+
+ return {
+ ...user,
+ id,
+ oldestTimestamp: oldestUserTimestamp,
+ };
+ })
+ .value();
+
+ return {
+ ...emoji,
+ users: usersWithTimestamps,
+ // Just in case two emojis have the same timestamp, also combine the timestamp with the
+ // emojiName so that the order will always be the same. Without this, the order can be pretty random
+ // and shift around a little bit.
+ oldestTimestamp: (oldestEmojiTimestamp || emoji.createdAt) + emojiName,
+ };
+};
+
+/**
+ * Returns true if the accountID has reacted to the report action (with the given skin tone).
+ * Uses the NEW FORMAT for "emojiReactions"
+ * @param {String} accountID
+ * @param {Array