Skip to content

Commit 04d28af

Browse files
authored
Merge pull request #6498 from mananjadhav/feat/frequent-used-emojis
Added frequently used emojis support
2 parents 3b5c680 + c501a2d commit 04d28af

File tree

7 files changed

+150
-13
lines changed

7 files changed

+150
-13
lines changed

src/CONST.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ const CONST = {
289289
PREFERRED_LOCALE: 'preferredLocale',
290290
KYC_MIGRATION: 'expensify_migration_2020_04_28_RunKycVerifications',
291291
PREFERRED_EMOJI_SKIN_TONE: 'expensify_preferredEmojiSkinTone',
292+
FREQUENTLY_USED_EMOJIS: 'expensify_frequentlyUsedEmojis',
292293
},
293294
DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'},
294295
DEFAULT_ACCOUNT_DATA: {error: '', success: '', loading: false},
@@ -310,6 +311,10 @@ const CONST = {
310311

311312
EMOJI_SPACER: 'SPACER',
312313

314+
EMOJI_NUM_PER_ROW: 8,
315+
316+
EMOJI_FREQUENT_ROW_COUNT: 3,
317+
313318
LOGIN_TYPE: {
314319
PHONE: 'phone',
315320
EMAIL: 'email',

src/ONYXKEYS.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ export default {
133133
// Store preferred skintone for emoji
134134
PREFERRED_EMOJI_SKIN_TONE: 'preferredEmojiSkinTone',
135135

136+
// Store frequently used emojis for this user
137+
FREQUENTLY_USED_EMOJIS: 'frequentlyUsedEmojis',
138+
136139
// Stores Workspace ID that will be tied to reimbursement account during setup
137140
REIMBURSEMENT_ACCOUNT_WORKSPACE_ID: 'reimbursementAccountWorkspaceID',
138141

src/libs/EmojiUtils.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import _ from 'underscore';
2+
import lodashOrderBy from 'lodash/orderBy';
3+
import moment from 'moment';
24
import CONST from '../CONST';
5+
import * as User from './actions/User';
36

47
/**
58
* Get the unicode code of an emoji in base 16.
@@ -67,9 +70,97 @@ function isSingleEmoji(message) {
6770
return matchedUnicode === currentMessageUnicode;
6871
}
6972

73+
/**
74+
* Get the header indices based on the max emojis per row
75+
* @param {Object[]} emojis
76+
* @returns {Number[]}
77+
*/
78+
function getDynamicHeaderIndices(emojis) {
79+
const headerIndices = [];
80+
_.each(emojis, (emoji, index) => {
81+
if (!emoji.header) {
82+
return;
83+
}
84+
headerIndices.push(Math.floor(index / CONST.EMOJI_NUM_PER_ROW));
85+
});
86+
return headerIndices;
87+
}
88+
89+
/**
90+
* Get number of empty spaces to be filled to get equal emojis for every row
91+
* @param {Number} emojiCount
92+
* @returns {Object[]}
93+
*/
94+
function getDynamicSpacing(emojiCount) {
95+
const spacerEmojis = [];
96+
let modLength = CONST.EMOJI_NUM_PER_ROW - (emojiCount % CONST.EMOJI_NUM_PER_ROW);
97+
while (modLength > 0) {
98+
spacerEmojis.push({
99+
code: CONST.EMOJI_SPACER,
100+
});
101+
modLength -= 1;
102+
}
103+
return spacerEmojis;
104+
}
105+
106+
/**
107+
* Get a merged array with frequently used emojis
108+
* @param {Object[]} emojis
109+
* @param {Object[]} frequentlyUsedEmojis
110+
* @returns {Object[]}
111+
*/
112+
function mergeEmojisWithFrequentlyUsedEmojis(emojis, frequentlyUsedEmojis = []) {
113+
if (frequentlyUsedEmojis.length === 0) {
114+
return emojis;
115+
}
116+
117+
let allEmojis = [{
118+
header: true,
119+
code: 'Frequently Used',
120+
}];
121+
122+
allEmojis = allEmojis.concat(getDynamicSpacing(allEmojis.length));
123+
allEmojis = allEmojis.concat(frequentlyUsedEmojis, getDynamicSpacing(frequentlyUsedEmojis.length));
124+
allEmojis = allEmojis.concat(emojis);
125+
return allEmojis;
126+
}
127+
128+
/**
129+
* Update the frequently used emojis list by usage and sync with API
130+
* @param {Object[]} frequentlyUsedEmojis
131+
* @param {Object} newEmoji
132+
*/
133+
function addToFrequentlyUsedEmojis(frequentlyUsedEmojis, newEmoji) {
134+
let frequentEmojiList = frequentlyUsedEmojis;
135+
let currentEmojiCount = 1;
136+
const currentTimestamp = moment().unix();
137+
const emojiIndex = _.findIndex(frequentEmojiList, e => e.code === newEmoji.code);
138+
if (emojiIndex >= 0) {
139+
currentEmojiCount = frequentEmojiList[emojiIndex].count + 1;
140+
frequentEmojiList.splice(emojiIndex, 1);
141+
}
142+
const updatedEmoji = {...newEmoji, ...{count: currentEmojiCount, lastUpdatedAt: currentTimestamp}};
143+
const maxFrequentEmojiCount = (CONST.EMOJI_FREQUENT_ROW_COUNT * CONST.EMOJI_NUM_PER_ROW) - 1;
144+
145+
// We want to make sure the current emoji is added to the list
146+
// Hence, we take one less than the current high frequent used emojis and if same then sorted by lastUpdatedAt
147+
frequentEmojiList = lodashOrderBy(frequentEmojiList, ['count', 'lastUpdatedAt'], ['desc', 'desc']);
148+
frequentEmojiList = frequentEmojiList.slice(0, maxFrequentEmojiCount);
149+
frequentEmojiList.push(updatedEmoji);
150+
151+
// Second sorting is required so that new emoji is properly placed at sort-ordered location
152+
frequentEmojiList = lodashOrderBy(frequentEmojiList, ['count', 'lastUpdatedAt'], ['desc', 'desc']);
153+
154+
User.setFrequentlyUsedEmojis(frequentEmojiList);
155+
}
156+
70157

71158
export {
72159
getEmojiUnicode,
73160
trimEmojiUnicode,
74161
isSingleEmoji,
162+
getDynamicHeaderIndices,
163+
getDynamicSpacing,
164+
mergeEmojisWithFrequentlyUsedEmojis,
165+
addToFrequentlyUsedEmojis,
75166
};

src/libs/actions/User.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ function getUserDetails() {
7777
nvpNames: [
7878
CONST.NVP.PAYPAL_ME_ADDRESS,
7979
CONST.NVP.PREFERRED_EMOJI_SKIN_TONE,
80+
CONST.NVP.FREQUENTLY_USED_EMOJIS,
8081
].join(','),
8182
})
8283
.then((response) => {
@@ -97,6 +98,9 @@ function getUserDetails() {
9798
const preferredSkinTone = lodashGet(response, `nameValuePairs.${CONST.NVP.PREFERRED_EMOJI_SKIN_TONE}`, {});
9899
Onyx.merge(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE,
99100
getSkinToneEmojiFromIndex(preferredSkinTone).skinTone);
101+
102+
const frequentlyUsedEmojis = lodashGet(response, `nameValuePairs.${CONST.NVP.FREQUENTLY_USED_EMOJIS}`, []);
103+
Onyx.set(ONYXKEYS.FREQUENTLY_USED_EMOJIS, frequentlyUsedEmojis);
100104
});
101105
}
102106

@@ -317,6 +321,15 @@ function setPreferredSkinTone(skinTone) {
317321
return NameValuePair.set(CONST.NVP.PREFERRED_EMOJI_SKIN_TONE, skinTone, ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE);
318322
}
319323

324+
/**
325+
* Sync frequentlyUsedEmojis with Onyx and Server
326+
* @param {Object[]} frequentlyUsedEmojis
327+
*/
328+
329+
function setFrequentlyUsedEmojis(frequentlyUsedEmojis) {
330+
return NameValuePair.set(CONST.NVP.FREQUENTLY_USED_EMOJIS, frequentlyUsedEmojis, ONYXKEYS.FREQUENTLY_USED_EMOJIS);
331+
}
332+
320333
/**
321334
* @param {Boolean} shouldUseSecureStaging
322335
*/
@@ -343,4 +356,5 @@ export {
343356
setShouldUseSecureStaging,
344357
clearUserErrorMessage,
345358
subscribeToExpensifyCardUpdates,
359+
setFrequentlyUsedEmojis,
346360
};

src/pages/home/report/EmojiPickerMenu/index.js

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import withLocalize, {withLocalizePropTypes} from '../../../../components/withLo
1515
import compose from '../../../../libs/compose';
1616
import getOperatingSystem from '../../../../libs/getOperatingSystem';
1717
import EmojiSkinToneList from '../EmojiSkinToneList';
18+
import * as EmojiUtils from '../../../../libs/EmojiUtils';
1819

1920
const propTypes = {
2021
/** Function to add the selected emoji to the main compose text input */
@@ -29,6 +30,12 @@ const propTypes = {
2930
/** Function to sync the selected skin tone with parent, onyx and nvp */
3031
updatePreferredSkinTone: PropTypes.func,
3132

33+
/** User's frequently used emojis */
34+
frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.shape({
35+
code: PropTypes.string.isRequired,
36+
keywords: PropTypes.arrayOf(PropTypes.string),
37+
})).isRequired,
38+
3239
/** Props related to the dimensions of the window */
3340
...windowDimensionsPropTypes,
3441

@@ -55,19 +62,20 @@ class EmojiPickerMenu extends Component {
5562
// For this reason to make headers work, we need to have the header be the only rendered element in its row
5663
// If this number is changed, emojis.js will need to be updated to have the proper number of spacer elements
5764
// around each header.
58-
this.numColumns = 8;
65+
this.numColumns = CONST.EMOJI_NUM_PER_ROW;
66+
67+
const allEmojis = EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis, this.props.frequentlyUsedEmojis);
5968

6069
// This is the indices of each category of emojis
6170
// The positions are static, and are calculated as index/numColumns (8 in our case)
6271
// This is because each row of 8 emojis counts as one index
63-
// If more emojis are ever added to emojis.js this will need to be updated or things will break
64-
this.unfilteredHeaderIndices = [0, 33, 59, 87, 98, 120, 147];
72+
this.unfilteredHeaderIndices = EmojiUtils.getDynamicHeaderIndices(allEmojis);
6573

6674
// If we're on Windows, don't display the flag emojis (the last category),
6775
// since Windows doesn't support them (and only displays country codes instead)
6876
this.emojis = getOperatingSystem() === CONST.OS.WINDOWS
69-
? emojis.slice(0, this.unfilteredHeaderIndices.pop() * this.numColumns)
70-
: emojis;
77+
? allEmojis.slice(0, this.unfilteredHeaderIndices.pop() * this.numColumns)
78+
: allEmojis;
7179

7280
this.filterEmojis = _.debounce(this.filterEmojis.bind(this), 300);
7381
this.highlightAdjacentEmoji = this.highlightAdjacentEmoji.bind(this);
@@ -119,7 +127,7 @@ class EmojiPickerMenu extends Component {
119127

120128
// Select the currently highlighted emoji if enter is pressed
121129
if (keyBoardEvent.key === 'Enter' && this.state.highlightedIndex !== -1) {
122-
this.props.onEmojiSelected(this.state.filteredEmojis[this.state.highlightedIndex].code);
130+
this.props.onEmojiSelected(this.state.filteredEmojis[this.state.highlightedIndex].code, this.state.filteredEmojis[this.state.highlightedIndex]);
123131
return;
124132
}
125133

@@ -346,7 +354,7 @@ class EmojiPickerMenu extends Component {
346354

347355
return (
348356
<EmojiPickerMenuItem
349-
onPress={this.props.onEmojiSelected}
357+
onPress={emoji => this.props.onEmojiSelected(emoji, item)}
350358
onHover={() => this.setState({highlightedIndex: index})}
351359
emoji={emojiCode}
352360
isHighlighted={index === this.state.highlightedIndex}

src/pages/home/report/EmojiPickerMenu/index.native.js

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import EmojiPickerMenuItem from '../EmojiPickerMenuItem';
1010
import ExpensifyText from '../../../../components/ExpensifyText';
1111
import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize';
1212
import EmojiSkinToneList from '../EmojiSkinToneList';
13+
import * as EmojiUtils from '../../../../libs/EmojiUtils';
1314

1415
const propTypes = {
1516
/** Function to add the selected emoji to the main compose text input */
@@ -21,6 +22,13 @@ const propTypes = {
2122
/** Function to sync the selected skin tone with parent, onyx and nvp */
2223
updatePreferredSkinTone: PropTypes.func,
2324

25+
/** User's frequently used emojis */
26+
frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.shape({
27+
code: PropTypes.string.isRequired,
28+
keywords: PropTypes.arrayOf(PropTypes.string),
29+
})).isRequired,
30+
31+
2432
/** Props related to the dimensions of the window */
2533
...windowDimensionsPropTypes,
2634

@@ -37,13 +45,14 @@ class EmojiPickerMenu extends Component {
3745
// For this reason to make headers work, we need to have the header be the only rendered element in its row
3846
// If this number is changed, emojis.js will need to be updated to have the proper number of spacer elements
3947
// around each header.
40-
this.numColumns = 8;
48+
this.numColumns = CONST.EMOJI_NUM_PER_ROW;
49+
50+
this.emojis = EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis, this.props.frequentlyUsedEmojis);
4151

4252
// This is the indices of each category of emojis
4353
// The positions are static, and are calculated as index/numColumns (8 in our case)
4454
// This is because each row of 8 emojis counts as one index
45-
// If this emojis are ever added to emojis.js this will need to be updated or things will break
46-
this.unfilteredHeaderIndices = [0, 33, 59, 87, 98, 120, 147];
55+
this.unfilteredHeaderIndices = EmojiUtils.getDynamicHeaderIndices(this.emojis);
4756

4857
this.renderItem = this.renderItem.bind(this);
4958
this.isMobileLandscape = this.isMobileLandscape.bind(this);
@@ -89,7 +98,7 @@ class EmojiPickerMenu extends Component {
8998

9099
return (
91100
<EmojiPickerMenuItem
92-
onPress={this.props.onEmojiSelected}
101+
onPress={emoji => this.props.onEmojiSelected(emoji, item)}
93102
emoji={emojiCode}
94103
/>
95104
);
@@ -100,7 +109,7 @@ class EmojiPickerMenu extends Component {
100109
return (
101110
<View style={styles.emojiPickerContainer}>
102111
<FlatList
103-
data={emojis}
112+
data={this.emojis}
104113
renderItem={this.renderItem}
105114
keyExtractor={item => (`emoji_picker_${item.code}`)}
106115
numColumns={this.numColumns}

src/pages/home/report/ReportActionCompose.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import ParticipantLocalTime from './ParticipantLocalTime';
4848
import {withNetwork, withPersonalDetails} from '../../../components/OnyxProvider';
4949
import DateUtils from '../../../libs/DateUtils';
5050
import Tooltip from '../../../components/Tooltip';
51+
import * as EmojiUtils from '../../../libs/EmojiUtils';
5152

5253
const propTypes = {
5354
/** Beta features list */
@@ -412,8 +413,10 @@ class ReportActionCompose extends React.Component {
412413
* Callback for the emoji picker to add whatever emoji is chosen into the main input
413414
*
414415
* @param {String} emoji
416+
* @param {Object} emojiObject
415417
*/
416-
addEmojiToTextBox(emoji) {
418+
addEmojiToTextBox(emoji, emojiObject) {
419+
EmojiUtils.addToFrequentlyUsedEmojis(this.props.frequentlyUsedEmojis, emojiObject);
417420
this.hideEmojiPicker();
418421
const newComment = this.comment.slice(0, this.state.selection.start)
419422
+ emoji + this.comment.slice(this.state.selection.end, this.comment.length);
@@ -669,6 +672,7 @@ class ReportActionCompose extends React.Component {
669672
ref={el => this.emojiSearchInput = el}
670673
preferredSkinTone={this.props.preferredSkinTone}
671674
updatePreferredSkinTone={this.setPreferredSkinTone}
675+
frequentlyUsedEmojis={this.props.frequentlyUsedEmojis}
672676
/>
673677
</Popover>
674678
<Pressable
@@ -757,5 +761,8 @@ export default compose(
757761
preferredSkinTone: {
758762
key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE,
759763
},
764+
frequentlyUsedEmojis: {
765+
key: ONYXKEYS.FREQUENTLY_USED_EMOJIS,
766+
},
760767
}),
761768
)(ReportActionCompose);

0 commit comments

Comments
 (0)