Skip to content
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

Support new emojiReaction data format #17996

Merged
merged 83 commits into from
Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
46f1cca
Create a separate collection for reactions
tgolen Apr 25, 2023
f5de44f
Call the new toggle method
tgolen Apr 25, 2023
a7945e9
Display reactions in new format
tgolen Apr 25, 2023
1b9f15f
Make sure toggling reactions works locally in onyx
tgolen Apr 25, 2023
af0fed2
Fix a few references to user account IDs and clean up code
tgolen Apr 25, 2023
b805586
Protect against report actions that don't have any reactions yet
tgolen Apr 26, 2023
4d70bbd
Turn on API requests
tgolen Apr 27, 2023
7698ad0
Merge branch 'main' into tgolen-migrate-reactions
tgolen Apr 27, 2023
8aacb9f
Simplify the reaction data
tgolen Apr 27, 2023
cf4a396
Use the updated data structure
tgolen Apr 27, 2023
5d0808b
Merge branch 'main' into tgolen-migrate-reactions
tgolen May 11, 2023
f88e62c
style
tgolen May 11, 2023
ea49f29
style
tgolen May 11, 2023
36568e2
Get new emoji reactions displaying properly
tgolen May 11, 2023
62f83fe
Add timestamp to requests when adding reactions
tgolen May 12, 2023
8c31f4f
Add API parameter to use new data format
tgolen May 12, 2023
f93266e
Rename methods
tgolen May 12, 2023
44d426a
Rename old toggle method
tgolen May 12, 2023
5b99716
Provide a second toggle method for emojiReactions
tgolen May 12, 2023
b2781c9
Update for checking to see if someone already reacted
tgolen May 12, 2023
9adef35
Merge branch 'main' into tgolen-migrate-reactions
tgolen May 15, 2023
c8369e5
style
tgolen May 15, 2023
d7221f6
Remove code that still uses old format
tgolen May 15, 2023
34797c7
Remove more code referencing old reactions
tgolen May 15, 2023
11c5380
Remove unused component
tgolen May 15, 2023
2970f79
Get tests working for toggling reactions
tgolen May 15, 2023
c17e2ec
Update tests for adding reactions
tgolen May 15, 2023
b8add23
Get emojis removing properly
tgolen May 15, 2023
a51ede1
Merge branch 'main' into tgolen-migrate-reactions
tgolen May 16, 2023
cb9c262
Fix optimistic removal of emoji
tgolen May 16, 2023
f35e7bc
WIP on preserving order of reactions
tgolen May 17, 2023
3de5adf
Merge branch 'main' into tgolen-migrate-reactions
tgolen May 17, 2023
62464bd
Apply changes from main
tgolen May 17, 2023
7d5eca7
Remove unused method
tgolen May 17, 2023
28b31c9
Sort the reactions by timestamp so that they are always in the same o…
tgolen May 17, 2023
02d333b
Merge branch 'main' into tgolen-migrate-reactions
tgolen May 23, 2023
2394a42
Update from recent code changes
tgolen May 23, 2023
2855cc0
Merge branch 'main' into tgolen-migrate-reactions
tgolen May 24, 2023
729f2aa
Remove debug
tgolen May 24, 2023
5ae68d6
Use negative values instead of strings
tgolen May 24, 2023
f26dacc
Remove unused parameter
tgolen May 24, 2023
270e166
Share proptypes
tgolen May 24, 2023
5f9dc09
Share more propTypes
tgolen May 24, 2023
9f26e87
Remove unused method
tgolen May 24, 2023
2994e00
Fix lint errors
tgolen May 25, 2023
b90fed1
Correct the tests when removing emoji
tgolen May 25, 2023
c24edaa
Disable eslint error
tgolen May 25, 2023
5bf57b3
Merge branch 'main' into tgolen-migrate-reactions
tgolen May 25, 2023
93012d7
Move ESLint disable to a better line
tgolen May 25, 2023
b518fcd
Add more data to the sorting key so that things will stay in order
tgolen May 25, 2023
53c3b98
Merge branch 'main' into tgolen-migrate-reactions
tgolen May 29, 2023
e422147
Simplify onyx key for collection
tgolen May 29, 2023
3d6f5a2
Add comment about cleanup task
tgolen May 29, 2023
5227282
Rename method and fix proptypes
tgolen May 29, 2023
6d60bee
Make the sorting work a little more reliably
tgolen May 29, 2023
5cf209f
Replace prop that was mistakingly removed
tgolen May 29, 2023
07ef367
Fix lint problem with propTypes
tgolen May 29, 2023
149cb37
Fix prop reference and jsdocs
tgolen May 30, 2023
69a05a5
Fix other JSDocs
tgolen May 30, 2023
da7d9fc
Merge branch 'main' of github.com:Expensify/App into tgolen-migrate-r…
stitesExpensify Jul 5, 2023
f2cf706
Fix emojiReaction prop name
stitesExpensify Jul 5, 2023
d11717b
Fix memo check
stitesExpensify Jul 5, 2023
0c7396e
Merge branch 'main' of github.com:Expensify/App into tgolen-migrate-r…
stitesExpensify Jul 5, 2023
bf7f6f7
Fix test
stitesExpensify Jul 5, 2023
ccf50b0
Lint
stitesExpensify Jul 5, 2023
979066a
Actually fix tests
stitesExpensify Jul 5, 2023
0ed88fd
Fix counting of skintones
stitesExpensify Jul 5, 2023
f5fb5a1
Remove unnecessary lodash get
stitesExpensify Jul 5, 2023
9d6e4e0
Fix quick reactions not properly removing emojis
stitesExpensify Jul 5, 2023
86d836a
Only get users who have reacted to display tooltip, and pass as number
stitesExpensify Jul 5, 2023
f6593a4
Simplify counting and update variable name
stitesExpensify Jul 6, 2023
aa0c587
Fix bad check that would fail for skintone==0
stitesExpensify Jul 6, 2023
47e6505
Fix bad merge
stitesExpensify Jul 6, 2023
88e6d1a
Fix param documentation
stitesExpensify Jul 6, 2023
ae31431
Update name to follow conventions
stitesExpensify Jul 6, 2023
066ad58
Check for undefined to account for 0
stitesExpensify Jul 7, 2023
f77421e
Start fixing merge conflicts
stitesExpensify Jul 10, 2023
846d059
Finish merge
stitesExpensify Jul 10, 2023
a9e26e5
Style
stitesExpensify Jul 10, 2023
c46bd4b
Merge branch 'main' of github.com:Expensify/App into tgolen-migrate-r…
stitesExpensify Jul 10, 2023
6d89fa9
Merge branch 'main' of github.com:Expensify/App into tgolen-migrate-r…
stitesExpensify Jul 14, 2023
b4d7e6e
Merge branch 'main' of github.com:Expensify/App into tgolen-migrate-r…
stitesExpensify Jul 17, 2023
73ca137
Remove unused prop
stitesExpensify Jul 17, 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 @@ -174,6 +174,7 @@ const CONST = {
},
DATE: {
MOMENT_FORMAT_STRING: 'YYYY-MM-DD',
SQL_DATE_TIME: 'YYYY-MM-DD HH:mm:ss',
UNIX_EPOCH: '1970-01-01 00:00:00.000',
MAX_DATE: '9999-12-31',
MIN_DATE: '0001-01-01',
Expand Down
6 changes: 5 additions & 1 deletion src/components/AttachmentCarousel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,11 @@ class AttachmentCarousel extends React.Component {

/**
* Defines how a single attachment should be rendered
* @param {{ isAuthTokenRequired: Boolean, source: String, file: { name: String } }} item
* @param {Object} item
* @param {Boolean} item.isAuthTokenRequired
* @param {String} item.source
* @param {Object} item.file
* @param {String} item.file.name
* @returns {JSX.Element}
*/
renderItem({item}) {
Expand Down
3 changes: 2 additions & 1 deletion src/components/AttachmentPicker/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,8 @@ class AttachmentPicker extends Component {
/**
* Setup native attachment selection to start after this popover closes
*
* @param {{pickAttachment: function}} item - an item from this.menuItemData
* @param {Object} item - an item from this.menuItemData
* @param {Function} item.pickAttachment
*/
selectItem(item) {
/* setTimeout delays execution to the frame after the modal closes
Expand Down
31 changes: 31 additions & 0 deletions src/components/Reactions/EmojiReactionsPropTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import PropTypes from 'prop-types';

/** All the emoji reactions for the report action. An object that looks like this:
"emojiReactions": {
"+1": { // The emoji added to the action
"createdAt": "2021-01-01 00:00:00",
"users": {
2352342: { // The accountID of the user who added this emoji
"skinTones": {
"1": "2021-01-01 00:00:00",
"2": "2021-01-01 00:00:00",
},
},
},
},
},
*/
export default PropTypes.objectOf(
PropTypes.shape({
/** The time the emoji was added */
createdAt: PropTypes.string,

/** All the users who have added this emoji */
users: PropTypes.objectOf(
PropTypes.shape({
/** The skin tone which was used and also the timestamp of when it was added */
stitesExpensify marked this conversation as resolved.
Show resolved Hide resolved
skinTones: PropTypes.objectOf(PropTypes.string),
}),
),
}),
);
13 changes: 10 additions & 3 deletions src/components/Reactions/MiniQuickEmojiReactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Icon from '../Icon';
import * as Expensicons from '../Icon/Expensicons';
import getButtonState from '../../libs/getButtonState';
import * as EmojiPickerAction from '../../libs/actions/EmojiPickerAction';
import {baseQuickEmojiReactionsPropTypes} from './QuickEmojiReactions/BaseQuickEmojiReactions';
import {baseQuickEmojiReactionsPropTypes, baseQuickEmojiReactionsDefaultProps} from './QuickEmojiReactions/BaseQuickEmojiReactions';
import withLocalize, {withLocalizePropTypes} from '../withLocalize';
import compose from '../../libs/compose';
import ONYXKEYS from '../../ONYXKEYS';
Expand Down Expand Up @@ -40,6 +40,7 @@ const propTypes = {
};

const defaultProps = {
...baseQuickEmojiReactionsDefaultProps,
onEmojiPickerClosed: () => {},
preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE,
reportAction: {},
Expand All @@ -61,7 +62,7 @@ function MiniQuickEmojiReactions(props) {
EmojiPickerAction.showEmojiPicker(
props.onEmojiPickerClosed,
(emojiCode, emojiObject) => {
props.onEmojiSelected(emojiObject);
props.onEmojiSelected(emojiObject, props.emojiReactions);
},
ref.current,
undefined,
Expand All @@ -77,7 +78,7 @@ function MiniQuickEmojiReactions(props) {
key={emoji.name}
isDelayButtonStateComplete={false}
tooltipText={`:${EmojiUtils.getLocalizedEmojiName(emoji.name, props.preferredLocale)}:`}
onPress={Session.checkIfActionIsAllowed(() => props.onEmojiSelected(emoji))}
onPress={Session.checkIfActionIsAllowed(() => props.onEmojiSelected(emoji, props.emojiReactions))}
>
<Text style={[styles.miniQuickEmojiReactionText, styles.userSelectNone]}>{EmojiUtils.getPreferredEmojiCode(emoji, props.preferredSkinTone)}</Text>
</BaseMiniContextMenuItem>
Expand Down Expand Up @@ -105,9 +106,15 @@ MiniQuickEmojiReactions.propTypes = propTypes;
MiniQuickEmojiReactions.defaultProps = defaultProps;
export default compose(
withLocalize,
// ESLint throws an error because it can't see that emojiReactions is defined in props. It is defined in props, but
// because of a couple spread operators, I think that's why ESLint struggles to see it
// eslint-disable-next-line rulesdir/onyx-props-must-have-default
withOnyx({
preferredSkinTone: {
key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE,
},
emojiReactions: {
key: ({reportActionID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`,
},
}),
)(MiniQuickEmojiReactions);
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ import styles from '../../../styles/styles';
import ONYXKEYS from '../../../ONYXKEYS';
import Tooltip from '../../Tooltip';
import * as EmojiUtils from '../../../libs/EmojiUtils';
import EmojiReactionsPropTypes from '../EmojiReactionsPropTypes';
import * as Session from '../../../libs/actions/Session';

const baseQuickEmojiReactionsPropTypes = {
emojiReactions: EmojiReactionsPropTypes,

/**
* Callback to fire when an emoji is selected.
*/
Expand All @@ -39,6 +42,7 @@ const baseQuickEmojiReactionsPropTypes = {
};

const baseQuickEmojiReactionsDefaultProps = {
emojiReactions: {},
onWillShowPicker: () => {},
onPressOpenPicker: () => {},
reportAction: {},
Expand Down Expand Up @@ -67,7 +71,7 @@ function BaseQuickEmojiReactions(props) {
<EmojiReactionBubble
emojiCodes={[EmojiUtils.getPreferredEmojiCode(emoji, props.preferredSkinTone)]}
isContextMenu
onPress={Session.checkIfActionIsAllowed(() => props.onEmojiSelected(emoji))}
onPress={Session.checkIfActionIsAllowed(() => props.onEmojiSelected(emoji, props.emojiReactions))}
/>
</View>
</Tooltip>
Expand All @@ -90,9 +94,12 @@ export default withOnyx({
preferredSkinTone: {
key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE,
},
emojiReactions: {
key: ({reportActionID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`,
},
preferredLocale: {
key: ONYXKEYS.NVP_PREFERRED_LOCALE,
},
})(BaseQuickEmojiReactions);

export {baseQuickEmojiReactionsPropTypes};
export {baseQuickEmojiReactionsPropTypes, baseQuickEmojiReactionsDefaultProps};
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,14 @@ import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultPro
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';
import * as EmojiUtils from '../../libs/EmojiUtils';
import ReportScreenContext from '../../pages/home/ReportScreenContext';

const propTypes = {
/**
* An array of objects containing the reaction data.
* The shape of a reaction looks like this:
*
* "reactionName": {
* emoji: string,
* users: {
* accountID: string,
* skinTone: number,
* }[]
* }
*/
// eslint-disable-next-line react/forbid-prop-types
reactions: PropTypes.arrayOf(PropTypes.object).isRequired,
emojiReactions: EmojiReactionsPropTypes,

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

const defaultProps = {
...withCurrentUserPersonalDetailsDefaultProps,
emojiReactions: {},
};

function ReportActionItemReactions(props) {
function ReportActionItemEmojiReactions(props) {
const {reactionListRef} = useContext(ReportScreenContext);
const popoverReactionListAnchor = useRef(null);
const reactionsWithCount = _.filter(props.reactions, (reaction) => reaction.users.length > 0);
let totalReactionCount = 0;

// 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;
});

return (
<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);
const emoji = EmojiUtils.findEmojiByName(reaction.emoji);
const emojiCodes = EmojiUtils.getUniqueEmojiCodes(emoji, reaction.users);
const hasUserReacted = Report.hasAccountIDReacted(props.currentUserPersonalDetails.accountID, reactionUsers);
{_.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(emoji);
props.toggleReaction(emojiAsset);
};

const onReactionListOpen = (event) => {
Expand All @@ -76,14 +103,14 @@ function ReportActionItemReactions(props) {
<Tooltip
renderTooltipContent={() => (
<ReactionTooltipContent
emojiName={EmojiUtils.getLocalizedEmojiName(reaction.emoji, props.preferredLocale)}
emojiName={EmojiUtils.getLocalizedEmojiName(reactionEmojiName, props.preferredLocale)}
emojiCodes={emojiCodes}
accountIDs={reactionUsers}
accountIDs={reactionUserAccountIDs}
currentUserPersonalDetails={props.currentUserPersonalDetails}
/>
)}
renderTooltipContentKey={[..._.map(reactionUsers, (user) => user.toString()), ...emojiCodes]}
key={reaction.emoji}
key={reactionEmojiName}
>
<View>
<EmojiReactionBubble
Expand All @@ -99,7 +126,7 @@ function ReportActionItemReactions(props) {
</Tooltip>
);
})}
{reactionsWithCount.length > 0 && (
{totalReactionCount > 0 && (
<AddReactionBubble
onSelectEmoji={props.toggleReaction}
reportAction={{reportActionID: props.reportActionID}}
Expand All @@ -109,7 +136,7 @@ function ReportActionItemReactions(props) {
);
}

ReportActionItemReactions.displayName = 'ReportActionItemReactions';
ReportActionItemReactions.propTypes = propTypes;
ReportActionItemReactions.defaultProps = defaultProps;
export default compose(withLocalize, withCurrentUserPersonalDetails)(ReportActionItemReactions);
ReportActionItemEmojiReactions.displayName = 'ReportActionItemReactions';
ReportActionItemEmojiReactions.propTypes = propTypes;
ReportActionItemEmojiReactions.defaultProps = defaultProps;
export default compose(withLocalize, withCurrentUserPersonalDetails)(ReportActionItemEmojiReactions);
4 changes: 3 additions & 1 deletion src/components/ThumbnailImage.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ class ThumbnailImage extends PureComponent {
/**
* Update the state with the computed thumbnail sizes.
*
* @param {{ width: number, height: number }} Params - width and height of the original image.
* @param {Object} Params - width and height of the original image.
* @param {Number} Params.width
* @param {Number} Params.height
*/
updateImageSize({width, height}) {
const {thumbnailWidth, thumbnailHeight} = this.calculateThumbnailImageSize(width, height);
Expand Down
25 changes: 15 additions & 10 deletions src/libs/EmojiUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import _ from 'underscore';
import moment from 'moment';
import Str from 'expensify-common/lib/str';
import Onyx from 'react-native-onyx';
import lodashGet from 'lodash/get';
import ONYXKEYS from '../ONYXKEYS';
import CONST from '../CONST';
import emojisTrie from './EmojiTrie';
Expand Down Expand Up @@ -384,20 +385,24 @@ 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.
* @param {{ name: string, code: string, types: string[] }} emoji
* @param {Object} emojiAsset
* @param {String} emojiAsset.name
* @param {String} emojiAsset.code
* @param {String[]} [emojiAsset.types]
* @param {Array} users
* @return {string[]}
* */
const getUniqueEmojiCodes = (emoji, users) => {
const emojiCodes = [];
_.forEach(users, (user) => {
const emojiCode = getPreferredEmojiCode(emoji, user.skinTone);

if (emojiCode && !emojiCodes.includes(emojiCode)) {
emojiCodes.push(emojiCode);
}
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);
}
});
});
return emojiCodes;
return uniqueEmojiCodes;
};

export {
Expand Down
Loading