@atlaskit/emoji 65.1.1 → 65.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/cjs/components/common/CachingEmoji.js +84 -151
  3. package/dist/cjs/components/common/Emoji.js +2 -2
  4. package/dist/cjs/components/common/EmojiActions.js +129 -175
  5. package/dist/cjs/components/common/EmojiErrorMessage.js +23 -59
  6. package/dist/cjs/components/common/EmojiPreviewComponent.js +1 -0
  7. package/dist/cjs/components/common/EmojiUploadPicker.js +235 -293
  8. package/dist/cjs/components/common/FileChooser.js +34 -71
  9. package/dist/cjs/components/common/Popup.js +105 -154
  10. package/dist/cjs/components/common/RetryableButton.js +43 -64
  11. package/dist/cjs/components/common/ToneSelector.js +50 -89
  12. package/dist/cjs/components/common/styles.js +14 -16
  13. package/dist/cjs/components/hooks.js +16 -0
  14. package/dist/cjs/components/picker/EmojiPickerCategoryHeading.js +16 -48
  15. package/dist/cjs/components/picker/EmojiPickerComponent.js +496 -516
  16. package/dist/cjs/components/picker/EmojiPickerEmojiRow.js +33 -60
  17. package/dist/cjs/components/picker/EmojiPickerFooter.js +13 -48
  18. package/dist/cjs/components/uploader/EmojiUploadComponent.js +124 -109
  19. package/dist/cjs/hooks/useEmojiContext.js +16 -0
  20. package/dist/cjs/hooks/{index.js → usePrevious.js} +0 -0
  21. package/dist/cjs/index.js +16 -0
  22. package/dist/cjs/version.json +1 -1
  23. package/dist/es2019/components/common/CachingEmoji.js +65 -112
  24. package/dist/es2019/components/common/Emoji.js +2 -2
  25. package/dist/es2019/components/common/EmojiActions.js +124 -150
  26. package/dist/es2019/components/common/EmojiErrorMessage.js +22 -26
  27. package/dist/es2019/components/common/EmojiPreviewComponent.js +1 -0
  28. package/dist/es2019/components/common/EmojiUploadPicker.js +190 -253
  29. package/dist/es2019/components/common/FileChooser.js +37 -40
  30. package/dist/es2019/components/common/Popup.js +89 -109
  31. package/dist/es2019/components/common/RetryableButton.js +43 -34
  32. package/dist/es2019/components/common/ToneSelector.js +46 -59
  33. package/dist/es2019/components/common/styles.js +9 -9
  34. package/dist/es2019/components/hooks.js +8 -0
  35. package/dist/es2019/components/picker/EmojiPickerCategoryHeading.js +13 -17
  36. package/dist/es2019/components/picker/EmojiPickerComponent.js +417 -506
  37. package/dist/es2019/components/picker/EmojiPickerEmojiRow.js +32 -35
  38. package/dist/es2019/components/picker/EmojiPickerFooter.js +11 -21
  39. package/dist/es2019/components/uploader/EmojiUploadComponent.js +81 -91
  40. package/dist/es2019/hooks/useEmojiContext.js +3 -0
  41. package/dist/es2019/hooks/{index.js → usePrevious.js} +0 -0
  42. package/dist/es2019/index.js +4 -1
  43. package/dist/es2019/version.json +1 -1
  44. package/dist/esm/components/common/CachingEmoji.js +86 -156
  45. package/dist/esm/components/common/Emoji.js +2 -2
  46. package/dist/esm/components/common/EmojiActions.js +129 -176
  47. package/dist/esm/components/common/EmojiErrorMessage.js +21 -56
  48. package/dist/esm/components/common/EmojiPreviewComponent.js +1 -0
  49. package/dist/esm/components/common/EmojiUploadPicker.js +233 -299
  50. package/dist/esm/components/common/FileChooser.js +34 -70
  51. package/dist/esm/components/common/Popup.js +104 -155
  52. package/dist/esm/components/common/RetryableButton.js +40 -60
  53. package/dist/esm/components/common/ToneSelector.js +50 -87
  54. package/dist/esm/components/common/styles.js +10 -10
  55. package/dist/esm/components/hooks.js +8 -0
  56. package/dist/esm/components/picker/EmojiPickerCategoryHeading.js +14 -43
  57. package/dist/esm/components/picker/EmojiPickerComponent.js +486 -535
  58. package/dist/esm/components/picker/EmojiPickerEmojiRow.js +31 -59
  59. package/dist/esm/components/picker/EmojiPickerFooter.js +14 -49
  60. package/dist/esm/components/uploader/EmojiUploadComponent.js +119 -113
  61. package/dist/esm/hooks/useEmojiContext.js +5 -0
  62. package/dist/esm/hooks/{index.js → usePrevious.js} +0 -0
  63. package/dist/esm/index.js +4 -1
  64. package/dist/esm/version.json +1 -1
  65. package/dist/types/components/common/CachingEmoji.d.ts +3 -13
  66. package/dist/types/components/common/Emoji.d.ts +1 -1
  67. package/dist/types/components/common/EmojiActions.d.ts +6 -17
  68. package/dist/types/components/common/EmojiErrorMessage.d.ts +3 -6
  69. package/dist/types/components/common/EmojiPreviewComponent.d.ts +2 -2
  70. package/dist/types/components/common/EmojiUploadPicker.d.ts +3 -27
  71. package/dist/types/components/common/FileChooser.d.ts +3 -5
  72. package/dist/types/components/common/Popup.d.ts +3 -20
  73. package/dist/types/components/common/RetryableButton.d.ts +3 -7
  74. package/dist/types/components/common/ToneSelector.d.ts +4 -10
  75. package/dist/types/components/common/setSkinToneAriaLabelText.d.ts +1 -1
  76. package/dist/types/components/common/styles.d.ts +1 -3
  77. package/dist/types/components/hooks.d.ts +1 -0
  78. package/dist/types/components/picker/CategorySelector.d.ts +1 -1
  79. package/dist/types/components/picker/EmojiPicker.d.ts +3 -3
  80. package/dist/types/components/picker/EmojiPickerCategoryHeading.d.ts +3 -4
  81. package/dist/types/components/picker/EmojiPickerComponent.d.ts +4 -80
  82. package/dist/types/components/picker/EmojiPickerEmojiRow.d.ts +3 -4
  83. package/dist/types/components/picker/EmojiPickerFooter.d.ts +3 -6
  84. package/dist/types/components/uploader/EmojiUploadComponent.d.ts +3 -17
  85. package/dist/types/components/uploader/EmojiUploader.d.ts +5 -7
  86. package/dist/types/hooks/useEmojiContext.d.ts +1 -0
  87. package/dist/types/hooks/{index.d.ts → usePrevious.d.ts} +0 -0
  88. package/dist/types/index.d.ts +3 -1
  89. package/dist/types/types.d.ts +2 -1
  90. package/local-config-example.ts +3 -1
  91. package/package.json +17 -4
  92. package/dist/cjs/components/common/EmojiPreview.js +0 -194
  93. package/dist/es2019/components/common/EmojiPreview.js +0 -152
  94. package/dist/esm/components/common/EmojiPreview.js +0 -170
  95. package/dist/types/components/common/EmojiPreview.d.ts +0 -31
@@ -1,8 +1,7 @@
1
- import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
-
3
1
  /** @jsx jsx */
4
2
  import { jsx } from '@emotion/core';
5
- import { PureComponent } from 'react';
3
+ import { useCallback, useEffect, useMemo, useRef, useState, createRef, memo } from 'react';
4
+ import { unstable_batchedUpdates as batchedUpdates } from 'react-dom';
6
5
  import { FormattedMessage } from 'react-intl-next';
7
6
  import { getEmojiVariation } from '../../api/EmojiRepository';
8
7
  import { supportsUploadFeature } from '../../api/EmojiResource';
@@ -20,575 +19,487 @@ import EmojiPickerList from './EmojiPickerList';
20
19
  import { createAndFireEventInElementsChannel, categoryClickedEvent, closedPickerEvent, deleteBeginEvent, deleteCancelEvent, deleteConfirmEvent, openedPickerEvent, pickerClickedEvent, pickerSearchedEvent, selectedFileEvent, uploadBeginButton, uploadCancelButton, uploadConfirmButton, toneSelectorClosedEvent, ufoExperiences } from '../../util/analytics';
21
20
  import { emojiPicker } from './styles';
22
21
  import LegacyEmojiContextProvider from '../../context/LegacyEmojiContextProvider';
22
+ import { useDidMount } from '../hooks';
23
23
  const FREQUENTLY_USED_MAX = 16;
24
- export default class EmojiPickerComponent extends PureComponent {
25
- constructor(props) {
26
- super(props);
27
-
28
- _defineProperty(this, "onEmojiActive", (_emojiId, emoji) => {
29
- if (this.state.selectedEmoji !== emoji) {
30
- this.setState({
31
- selectedEmoji: emoji
32
- });
33
- }
34
- });
35
24
 
36
- _defineProperty(this, "onCategoryActivated", category => {
37
- if (this.state.activeCategory !== category) {
38
- this.setState({
39
- activeCategory: category
40
- });
41
- }
25
+ const EmojiPickerComponent = ({
26
+ emojiProvider,
27
+ onSelection,
28
+ onPickerRef,
29
+ hideToneSelector,
30
+ createAnalyticsEvent
31
+ }) => {
32
+ const [filteredEmojis, setFilteredEmojis] = useState([]);
33
+ const [searchEmojis, setSearchEmojis] = useState([]);
34
+ const [frequentlyUsedEmojis, setFrequentlyUsedEmojis] = useState([]);
35
+ const [query, setQuery] = useState('');
36
+ const [dynamicCategories, setDynamicCategories] = useState([]);
37
+ const [selectedTone, setSelectedTone] = useState(!hideToneSelector ? emojiProvider.getSelectedTone() : undefined);
38
+ const [loading, setLoading] = useState(true);
39
+ const [uploadSupported, setUploadSupported] = useState(false);
40
+ const [uploading, setUploading] = useState(false);
41
+ const [selectedEmoji, setSelectedEmoji] = useState();
42
+ const [activeCategory, setActiveCategory] = useState(null);
43
+ const [disableCategories, setDisableCategories] = useState(false);
44
+ const [uploadErrorMessage, setUploadErrorMessage] = useState();
45
+ const [emojiToDelete, setEmojiToDelete] = useState();
46
+ const [toneEmoji, setToneEmoji] = useState();
47
+ const emojiPickerList = useMemo(() => /*#__PURE__*/createRef(), []);
48
+ const openTime = useRef(0);
49
+ const isMounting = useRef(true);
50
+ const didMount = useDidMount();
51
+ const updateAfterDidMount = useRef(true);
52
+ const previousEmojiProvider = useRef(emojiProvider);
53
+ const currentUser = useMemo(() => {
54
+ return emojiProvider.getCurrentUser();
55
+ }, [emojiProvider]);
56
+ const fireAnalytics = useCallback(analyticsEvent => {
57
+ if (createAnalyticsEvent) {
58
+ createAndFireEventInElementsChannel(analyticsEvent)(createAnalyticsEvent);
59
+ }
60
+ }, [createAnalyticsEvent]);
61
+ const onEmojiActive = useCallback((emojiId, emoji) => {
62
+ if (!selectedEmoji || selectedEmoji.id !== (emojiId === null || emojiId === void 0 ? void 0 : emojiId.id)) {
63
+ setSelectedEmoji(emoji);
64
+ }
65
+ }, [selectedEmoji]);
66
+ const onCategoryActivated = useCallback(category => {
67
+ if (activeCategory !== category) {
68
+ setActiveCategory(category);
69
+ }
70
+ }, [activeCategory]);
71
+
72
+ const calculateElapsedTime = () => {
73
+ return Date.now() - openTime.current;
74
+ };
75
+
76
+ const onUploadSupported = useCallback(supported => {
77
+ setUploadSupported(supported);
78
+ }, []);
79
+ const onDynamicCategoryChange = useCallback(categories => {
80
+ setDynamicCategories(categories);
81
+ }, []);
82
+ const onUploadCancelled = useCallback(() => {
83
+ batchedUpdates(() => {
84
+ setUploading(false);
85
+ setUploadErrorMessage(undefined);
42
86
  });
87
+ fireAnalytics(uploadCancelButton());
88
+ }, [fireAnalytics]);
89
+ const getDynamicCategories = useCallback(() => {
90
+ if (!emojiProvider.calculateDynamicCategories) {
91
+ return Promise.resolve([]);
92
+ }
43
93
 
44
- _defineProperty(this, "onCategorySelected", categoryId => {
45
- const {
46
- emojiProvider
47
- } = this.props;
48
-
49
- if (!categoryId) {
50
- return;
51
- }
52
-
53
- emojiProvider.findInCategory(categoryId).then(emojisInCategory => {
54
- const {
55
- disableCategories
56
- } = this.state;
57
-
58
- if (!disableCategories) {
59
- let selectedEmoji;
60
-
61
- if (emojisInCategory && emojisInCategory.length > 0) {
62
- selectedEmoji = getEmojiVariation(emojisInCategory[0], {
63
- skinTone: this.state.selectedTone
64
- });
65
- }
66
-
67
- const emojiPickerList = this.refs.emojiPickerList;
68
-
69
- if (emojiPickerList) {
70
- emojiPickerList.reveal(categoryId);
71
- }
72
-
73
- this.setState({
74
- activeCategory: categoryId,
75
- selectedEmoji
76
- });
77
- this.fireAnalytics(categoryClickedEvent({
78
- category: categoryId
79
- }));
80
- }
81
- });
82
- });
94
+ return emojiProvider.calculateDynamicCategories();
95
+ }, [emojiProvider]);
96
+ /**
97
+ * Calculate and set the new state of the component in response to the list of emoji changing for some reason (a search has returned
98
+ * or the frequently used emoji have updated.)
99
+ */
83
100
 
84
- _defineProperty(this, "onPreviewDisplayed", isDisplayed => {
85
- this.setState({
86
- isPreviewDisplayed: isDisplayed
101
+ const setStateAfterEmojiChange = useCallback(({
102
+ searchQuery,
103
+ emojiToRender,
104
+ searchEmoji,
105
+ frequentEmoji
106
+ }) => {
107
+ // Only enable categories for full emoji list (non-search)
108
+ const disableCategories = !!searchQuery;
109
+
110
+ if (!disableCategories && emojiToRender && emojiToRender.length !== filteredEmojis.length) {
111
+ getDynamicCategories().then(categories => {
112
+ onDynamicCategoryChange(categories);
87
113
  });
88
- });
89
-
90
- _defineProperty(this, "onFileChooserClicked", () => {
91
- this.fireAnalytics(selectedFileEvent());
92
- });
93
-
94
- _defineProperty(this, "fireAnalytics", analyticsEvent => {
95
- const {
96
- createAnalyticsEvent
97
- } = this.props;
98
-
99
- if (createAnalyticsEvent) {
100
- createAndFireEventInElementsChannel(analyticsEvent)(createAnalyticsEvent);
101
- }
102
- });
114
+ }
103
115
 
104
- _defineProperty(this, "calculateElapsedTime", () => {
105
- return Date.now() - this.openTime;
106
- });
116
+ if (emojiToRender && !containsEmojiId(emojiToRender, selectedEmoji)) {
117
+ batchedUpdates(() => {
118
+ setSelectedEmoji(undefined); // Only enable categories for full emoji list (non-search)
107
119
 
108
- _defineProperty(this, "onUploadSupported", supported => {
109
- this.setState({
110
- uploadSupported: supported
120
+ setActiveCategory(null);
111
121
  });
112
- });
113
-
114
- _defineProperty(this, "onSearch", query => {
115
- const options = {
116
- skinTone: this.state.selectedTone
117
- };
122
+ }
118
123
 
119
- if (query !== this.state.query) {
120
- ufoExperiences['emoji-searched'].start();
121
- ufoExperiences['emoji-searched'].addMetadata({
122
- queryLength: query.length,
123
- source: 'picker'
124
- });
124
+ batchedUpdates(() => {
125
+ if (emojiToRender) {
126
+ setFilteredEmojis(emojiToRender);
125
127
  }
126
128
 
127
- this.updateEmojis(query, options);
128
- });
129
-
130
- _defineProperty(this, "onSearchResult", searchResults => {
131
- const frequentlyUsedEmoji = this.state.frequentlyUsedEmojis || [];
132
- const searchQuery = searchResults.query || '';
133
- const emojiToRender = this.buildQuerySpecificEmojiList(searchQuery, searchResults.emojis, frequentlyUsedEmoji);
134
-
135
- if (searchQuery !== this.state.query) {
136
- this.fireAnalytics(pickerSearchedEvent({
137
- queryLength: searchQuery.length,
138
- numMatches: emojiToRender.length
139
- }));
140
- ufoExperiences['emoji-searched'].success({
141
- metadata: {
142
- emojisLength: emojiToRender.length
143
- }
144
- });
129
+ if (searchEmoji) {
130
+ setSearchEmojis(searchEmoji);
145
131
  }
146
132
 
147
- this.setStateAfterEmojiChange(searchQuery, emojiToRender, searchResults.emojis, frequentlyUsedEmoji);
148
- });
149
-
150
- _defineProperty(this, "onFrequentEmojiResult", frequentEmoji => {
151
- const {
152
- query,
153
- searchEmojis
154
- } = this.state; // change the category of each of the featured emoji
155
-
156
- const recategorised = frequentEmoji.map(emoji => {
157
- const clone = JSON.parse(JSON.stringify(emoji));
158
- clone.category = frequentCategory;
159
- return clone;
160
- });
161
- const emojiToRender = this.buildQuerySpecificEmojiList(query, searchEmojis, recategorised);
162
- this.setStateAfterEmojiChange(query, emojiToRender, searchEmojis, recategorised);
163
- });
133
+ if (frequentEmoji) {
134
+ setFrequentlyUsedEmojis(frequentEmoji);
135
+ }
164
136
 
165
- _defineProperty(this, "onDynamicCategoryChange", categories => {
166
- this.setState({
167
- dynamicCategories: categories
168
- });
137
+ setLoading(false);
138
+ setDisableCategories(disableCategories);
169
139
  });
170
-
171
- _defineProperty(this, "onProviderChange", {
172
- result: this.onSearchResult
140
+ }, [filteredEmojis.length, getDynamicCategories, onDynamicCategoryChange, selectedEmoji]);
141
+ const onFrequentEmojiResult = useCallback(frequentEmoji => {
142
+ // change the category of each of the featured emoji
143
+ const recategorised = frequentEmoji.map(emoji => {
144
+ const clone = JSON.parse(JSON.stringify(emoji));
145
+ clone.category = frequentCategory;
146
+ return clone;
173
147
  });
174
-
175
- _defineProperty(this, "onToneSelected", toneValue => {
176
- this.setState({
177
- selectedTone: toneValue
178
- });
179
- this.props.emojiProvider.setSelectedTone(toneValue);
180
- const {
181
- query = ''
182
- } = this.state;
183
- this.updateEmojis(query, {
184
- skinTone: toneValue
185
- });
148
+ setStateAfterEmojiChange({
149
+ frequentEmoji: recategorised
186
150
  });
151
+ }, [setStateAfterEmojiChange]);
152
+ const onSearchResult = useCallback(searchResults => {
153
+ const frequentlyUsedEmoji = frequentlyUsedEmojis || [];
154
+ const searchQuery = searchResults.query || '';
155
+ /**
156
+ * If there is no user search in the EmojiPicker then it should display all emoji received from the EmojiRepository and should
157
+ * also include a special category of most frequently used emoji (if there are any). This method decides if we are in this 'no search'
158
+ * state and appends the frequent emoji if necessary.
159
+ */
160
+
161
+ let emojiToRender;
162
+
163
+ if (!frequentlyUsedEmoji.length || query) {
164
+ emojiToRender = searchResults.emojis;
165
+ } else {
166
+ emojiToRender = [...searchResults.emojis, ...frequentlyUsedEmoji];
167
+ }
187
168
 
188
- _defineProperty(this, "onToneSelectorCancelled", () => {
189
- this.fireAnalytics(toneSelectorClosedEvent());
169
+ setStateAfterEmojiChange({
170
+ searchQuery,
171
+ emojiToRender,
172
+ searchEmoji: searchResults.emojis
190
173
  });
174
+ }, [frequentlyUsedEmojis, query, setStateAfterEmojiChange]);
175
+ const onProviderChange = useMemo(() => {
176
+ return {
177
+ result: onSearchResult
178
+ };
179
+ }, [onSearchResult]);
180
+ /**
181
+ * Updates the emoji displayed by the picker. If there is no query specified then we expect to retrieve all emoji for display,
182
+ * by category, in the picker. This differs from when there is a query in which case we expect to receive a sorted result matching
183
+ * the search.
184
+ */
191
185
 
192
- _defineProperty(this, "updateEmojis", (query, options) => {
193
- // if the query is empty then we want the emoji to be in service defined order, unless specified otherwise
194
- // and we want emoji for the 'frequently used' category to be refreshed as well.
195
- if (!query) {
196
- if (!options) {
197
- options = {};
198
- }
199
-
200
- if (!options.sort) {
201
- options.sort = SearchSort.None;
202
- } // take a copy of search options so that the frequently used can be limited to 16 without affecting the full emoji query
203
-
204
-
205
- const frequentOptions = { ...options,
206
- sort: SearchSort.None,
207
- limit: FREQUENTLY_USED_MAX
208
- };
209
- this.props.emojiProvider.getFrequentlyUsed(frequentOptions).then(this.onFrequentEmojiResult);
186
+ const updateEmojis = useCallback((query, options) => {
187
+ // if the query is empty then we want the emoji to be in service defined order, unless specified otherwise
188
+ // and we want emoji for the 'frequently used' category to be refreshed as well.
189
+ if (!query) {
190
+ if (!options) {
191
+ options = {};
210
192
  }
211
193
 
212
- this.props.emojiProvider.filter(query, options);
213
- });
194
+ if (!options.sort) {
195
+ options.sort = SearchSort.None;
196
+ } // take a copy of search options so that the frequently used can be limited to 16 without affecting the full emoji query
214
197
 
215
- _defineProperty(this, "onOpenUpload", () => {
216
- // Prime upload token so it's ready when the user adds
217
- const {
218
- emojiProvider
219
- } = this.props;
220
198
 
221
- if (supportsUploadFeature(emojiProvider)) {
222
- emojiProvider.prepareForUpload();
223
- }
199
+ const frequentOptions = { ...options,
200
+ sort: SearchSort.None,
201
+ limit: FREQUENTLY_USED_MAX
202
+ };
203
+ emojiProvider.getFrequentlyUsed(frequentOptions).then(onFrequentEmojiResult);
204
+ }
224
205
 
225
- this.setState({
226
- uploadErrorMessage: undefined,
227
- uploading: true
228
- });
229
- this.fireAnalytics(uploadBeginButton());
206
+ emojiProvider.filter(query, options);
207
+ }, [emojiProvider, onFrequentEmojiResult]);
208
+ const onToneSelected = useCallback(toneValue => {
209
+ emojiProvider.setSelectedTone(toneValue);
210
+ updateEmojis(query, {
211
+ skinTone: toneValue
230
212
  });
231
-
232
- _defineProperty(this, "onUploadEmoji", (upload, retry) => {
233
- const {
234
- emojiProvider
235
- } = this.props;
236
- this.fireAnalytics(uploadConfirmButton({
237
- retry
213
+ setSelectedTone(toneValue);
214
+ }, [emojiProvider, query, updateEmojis]);
215
+ const onToneSelectorCancelled = useCallback(() => {
216
+ fireAnalytics(toneSelectorClosedEvent());
217
+ }, [fireAnalytics]);
218
+ const onSelectWrapper = useCallback((emojiId, emoji, event) => {
219
+ if (onSelection) {
220
+ onSelection(emojiId, emoji, event);
221
+ fireAnalytics(pickerClickedEvent({
222
+ duration: calculateElapsedTime(),
223
+ emojiId: (emojiId === null || emojiId === void 0 ? void 0 : emojiId.id) || '',
224
+ category: emoji && emoji.category || '',
225
+ type: emoji && emoji.type || '',
226
+ queryLength: query && query.length || 0
238
227
  }));
228
+ }
229
+ }, [fireAnalytics, onSelection, query]);
230
+ const onCategorySelected = useCallback(categoryId => {
231
+ if (!categoryId) {
232
+ return;
233
+ }
239
234
 
240
- const errorSetter = message => {
241
- this.setState({
242
- uploadErrorMessage: message
243
- });
244
- };
235
+ emojiProvider.findInCategory(categoryId).then(emojisInCategory => {
236
+ if (!disableCategories) {
237
+ let newSelectedEmoji;
245
238
 
246
- const onSuccess = emojiDescription => {
247
- this.setState({
248
- activeCategory: customCategory,
249
- selectedEmoji: emojiDescription,
250
- uploading: false
251
- }); // this.loadEmoji(emojiProvider, emojiDescription);
239
+ if (emojisInCategory && emojisInCategory.length > 0) {
240
+ newSelectedEmoji = getEmojiVariation(emojisInCategory[0], {
241
+ skinTone: selectedTone
242
+ });
243
+ }
252
244
 
253
- this.scrollToEndOfList();
254
- };
245
+ if (emojiPickerList.current) {
246
+ emojiPickerList.current.reveal(categoryId);
247
+ }
255
248
 
256
- uploadEmoji(upload, emojiProvider, errorSetter, onSuccess, this.fireAnalytics, retry);
249
+ batchedUpdates(() => {
250
+ setActiveCategory(categoryId);
251
+ setSelectedEmoji(newSelectedEmoji);
252
+ });
253
+ fireAnalytics(categoryClickedEvent({
254
+ category: categoryId
255
+ }));
256
+ }
257
257
  });
258
+ }, [disableCategories, emojiPickerList, emojiProvider, fireAnalytics, selectedTone]);
259
+ const recordUsageOnSelection = useMemo(() => createRecordSelectionDefault(emojiProvider, onSelectWrapper, analytic => fireAnalytics(analytic('picker'))), [emojiProvider, fireAnalytics, onSelectWrapper]);
260
+ const formattedErrorMessage = useMemo(() => uploadErrorMessage ? jsx(FormattedMessage, uploadErrorMessage) : null, [uploadErrorMessage]);
261
+ const emojiContextValue = useMemo(() => ({
262
+ emoji: {
263
+ emojiProvider
264
+ }
265
+ }), [emojiProvider]);
266
+ const onFileChooserClicked = useCallback(() => {
267
+ fireAnalytics(selectedFileEvent());
268
+ }, [fireAnalytics]);
269
+ const onSearch = useCallback(searchQuery => {
270
+ const options = {
271
+ skinTone: selectedTone
272
+ };
258
273
 
259
- _defineProperty(this, "onTriggerDelete", (_emojiId, emoji) => {
260
- this.fireAnalytics(deleteBeginEvent({
261
- emojiId: _emojiId.id
262
- }));
263
- this.setState({
264
- emojiToDelete: emoji
274
+ if (query) {
275
+ ufoExperiences['emoji-searched'].start();
276
+ ufoExperiences['emoji-searched'].addMetadata({
277
+ queryLength: query.length,
278
+ source: 'picker'
265
279
  });
266
- });
280
+ }
267
281
 
268
- _defineProperty(this, "onCloseDelete", () => {
269
- const {
270
- emojiToDelete
271
- } = this.state;
272
- this.fireAnalytics(deleteCancelEvent({
273
- emojiId: emojiToDelete && emojiToDelete.id
274
- }));
275
- this.setState({
276
- emojiToDelete: undefined
277
- });
278
- });
282
+ if (searchQuery !== query) {
283
+ setQuery(searchQuery);
284
+ }
279
285
 
280
- _defineProperty(this, "onDeleteEmoji", emoji => {
281
- const {
282
- emojiToDelete,
283
- query,
284
- selectedTone
285
- } = this.state;
286
- this.fireAnalytics(deleteConfirmEvent({
287
- emojiId: emojiToDelete && emojiToDelete.id
288
- }));
289
- return this.props.emojiProvider.deleteSiteEmoji(emoji).then(success => {
290
- if (success) {
291
- this.updateEmojis(query, {
292
- skinTone: selectedTone
293
- });
294
- }
286
+ updateEmojis(query, options);
287
+ }, [query, selectedTone, updateEmojis]);
288
+ const onOpenUpload = useCallback(() => {
289
+ // Prime upload token so it's ready when the user adds
290
+ if (supportsUploadFeature(emojiProvider)) {
291
+ emojiProvider.prepareForUpload();
292
+ }
295
293
 
296
- return success;
297
- });
294
+ batchedUpdates(() => {
295
+ setUploadErrorMessage(undefined);
296
+ setUploading(true);
298
297
  });
298
+ fireAnalytics(uploadBeginButton());
299
+ }, [emojiProvider, fireAnalytics]);
300
+ const scrollToEndOfList = useCallback(() => {
301
+ if (typeof window === 'undefined') {
302
+ return;
303
+ }
299
304
 
300
- _defineProperty(this, "scrollToEndOfList", () => {
301
- const emojiPickerList = this.refs.emojiPickerList;
305
+ if (emojiPickerList.current) {
306
+ // Wait a tick to ensure repaint and updated height for picker list
307
+ window.setTimeout(() => {
308
+ var _emojiPickerList$curr;
302
309
 
303
- if (typeof window === 'undefined') {
304
- return;
305
- }
310
+ (_emojiPickerList$curr = emojiPickerList.current) === null || _emojiPickerList$curr === void 0 ? void 0 : _emojiPickerList$curr.scrollToBottom();
311
+ }, 0);
312
+ }
313
+ }, [emojiPickerList]);
314
+ const onUploadEmoji = useCallback((upload, retry) => {
315
+ fireAnalytics(uploadConfirmButton({
316
+ retry
317
+ }));
306
318
 
307
- if (emojiPickerList) {
308
- // Wait a tick to ensure repaint and updated height for picker list
309
- window.setTimeout(() => {
310
- emojiPickerList.scrollToBottom();
311
- }, 0);
312
- }
313
- });
319
+ const errorSetter = message => {
320
+ setUploadErrorMessage(message);
321
+ };
314
322
 
315
- _defineProperty(this, "onUploadCancelled", () => {
316
- this.setState({
317
- uploading: false,
318
- uploadErrorMessage: undefined
323
+ const onSuccess = emojiDescription => {
324
+ batchedUpdates(() => {
325
+ setActiveCategory(customCategory);
326
+ setSelectedEmoji(emojiDescription);
327
+ setUploading(false);
319
328
  });
320
- this.fireAnalytics(uploadCancelButton());
321
- });
329
+ scrollToEndOfList();
330
+ };
322
331
 
323
- _defineProperty(this, "handlePickerRef", ref => {
324
- if (this.props.onPickerRef) {
325
- this.props.onPickerRef(ref);
332
+ uploadEmoji(upload, emojiProvider, errorSetter, onSuccess, fireAnalytics, retry);
333
+ }, [emojiProvider, fireAnalytics, scrollToEndOfList]);
334
+ const onTriggerDelete = useCallback((_emojiId, emoji) => {
335
+ if (_emojiId) {
336
+ fireAnalytics(deleteBeginEvent({
337
+ emojiId: _emojiId.id
338
+ }));
339
+ setEmojiToDelete(emoji);
340
+ }
341
+ }, [fireAnalytics]);
342
+ const onCloseDelete = useCallback(() => {
343
+ fireAnalytics(deleteCancelEvent({
344
+ emojiId: emojiToDelete && emojiToDelete.id
345
+ }));
346
+ setEmojiToDelete(undefined);
347
+ }, [emojiToDelete, fireAnalytics]);
348
+ const onDeleteEmoji = useCallback(emoji => {
349
+ fireAnalytics(deleteConfirmEvent({
350
+ emojiId: emojiToDelete && emojiToDelete.id
351
+ }));
352
+ return emojiProvider.deleteSiteEmoji(emoji).then(success => {
353
+ if (success) {
354
+ updateEmojis(query, {
355
+ skinTone: selectedTone
356
+ });
326
357
  }
327
- });
328
358
 
329
- _defineProperty(this, "onSelectWrapper", (emojiId, emoji, event) => {
330
- const {
331
- onSelection
332
- } = this.props;
333
- const {
334
- query
335
- } = this.state;
336
-
337
- if (onSelection) {
338
- onSelection(emojiId, emoji, event);
339
- this.fireAnalytics(pickerClickedEvent({
340
- duration: this.calculateElapsedTime(),
341
- emojiId: emojiId.id || '',
342
- category: emoji && emoji.category || '',
343
- type: emoji && emoji.type || '',
344
- queryLength: query && query.length || 0
345
- }));
346
- }
359
+ return success;
347
360
  });
348
-
349
- const {
350
- emojiProvider: _emojiProvider,
351
- hideToneSelector
352
- } = props;
353
- this.state = {
354
- filteredEmojis: [],
355
- searchEmojis: [],
356
- frequentlyUsedEmojis: [],
357
- query: '',
358
- dynamicCategories: [],
359
- selectedTone: !hideToneSelector ? _emojiProvider.getSelectedTone() : undefined,
360
- loading: true,
361
- uploadSupported: false,
362
- uploading: false,
363
- isPreviewDisplayed: false
364
- };
365
- this.openTime = 0;
366
- }
367
-
368
- UNSAFE_componentWillMount() {
369
- ufoExperiences['emoji-picker-opened'].success();
370
- this.openTime = Date.now();
371
- this.fireAnalytics(openedPickerEvent());
372
- }
373
-
374
- componentDidMount() {
375
- const {
376
- emojiProvider,
377
- hideToneSelector
378
- } = this.props;
379
- emojiProvider.subscribe(this.onProviderChange);
380
- this.onSearch(this.state.query);
361
+ }, [emojiProvider, emojiToDelete, fireAnalytics, query, selectedTone, updateEmojis]);
362
+ const onComponentDidMount = useCallback(() => {
363
+ emojiProvider.subscribe(onProviderChange);
364
+ onSearch(query);
381
365
 
382
366
  if (supportsUploadFeature(emojiProvider)) {
383
- emojiProvider.isUploadSupported().then(this.onUploadSupported);
367
+ emojiProvider.isUploadSupported().then(onUploadSupported);
384
368
  }
385
369
 
386
370
  if (!hideToneSelector) {
387
371
  const toneEmoji = getToneEmoji(emojiProvider);
388
372
 
389
373
  if (isPromise(toneEmoji)) {
390
- toneEmoji.then(emoji => this.setState({
391
- toneEmoji: emoji
392
- }));
374
+ toneEmoji.then(emoji => setToneEmoji(emoji));
393
375
  } else if (toneEmoji === undefined || isEmojiDescription(toneEmoji)) {
394
- this.setState({
395
- toneEmoji
396
- });
397
- }
398
- }
399
- }
400
-
401
- componentWillUnmount() {
402
- const {
403
- emojiProvider
404
- } = this.props;
405
- emojiProvider.unsubscribe(this.onProviderChange);
406
- this.fireAnalytics(closedPickerEvent({
407
- duration: this.calculateElapsedTime()
408
- }));
409
- ufoExperiences['emoji-picker-opened'].abort({
410
- metadata: {
411
- source: 'EmojiPickerComponent',
412
- reason: 'unmount'
413
- }
414
- });
415
- ufoExperiences['emoji-searched'].abort({
416
- metadata: {
417
- source: 'EmojiPickerComponent',
418
- reason: 'unmount'
419
- }
420
- });
421
- }
422
-
423
- UNSAFE_componentWillReceiveProps(nextProps) {
424
- const prevEmojiProvider = this.props.emojiProvider;
425
- const nextEmojiProvider = nextProps.emojiProvider;
426
-
427
- if (prevEmojiProvider !== nextEmojiProvider) {
428
- if (supportsUploadFeature(nextEmojiProvider)) {
429
- nextEmojiProvider.isUploadSupported().then(this.onUploadSupported);
376
+ setToneEmoji(toneEmoji);
430
377
  }
431
378
  }
432
- }
433
-
434
- componentDidUpdate(prevProps) {
435
- const prevEmojiProvider = prevProps.emojiProvider;
436
- const currentEmojiProvider = this.props.emojiProvider;
437
-
438
- if (prevEmojiProvider !== currentEmojiProvider) {
439
- prevEmojiProvider.unsubscribe(this.onProviderChange);
440
- currentEmojiProvider.subscribe(this.onProviderChange); // We changed provider which means we subscribed to filter results for a new subscriber.
441
- // So we refresh the emoji display with onSearch and we do it here, after the new props have
442
- // been set since onSearch leads to filter being called on the current emojiProvider.
443
- // (Calling onSearch in a '...Will...' lifecycle method would lead to filter being called on
444
- // an emojiProvider we have already unsubscribed from)
445
-
446
- this.onSearch(this.state.query);
447
- }
448
- }
449
-
450
- /**
451
- * If there is no user search in the EmojiPicker then it should display all emoji received from the EmojiRepository and should
452
- * also include a special category of most frequently used emoji (if there are any). This method decides if we are in this 'no search'
453
- * state and appends the frequent emoji if necessary.
454
- *
455
- * @param searchEmoji the emoji last received from the EmojiRepository after a search (may be empty)
456
- * @param frequentEmoji the frequently used emoji last received from the EmojiRepository (may be empty)
457
- */
458
- buildQuerySpecificEmojiList(query, searchEmoji, frequentEmoji) {
459
- // If there are no frequent emoji, or if there was a search query then we want to take the search result exactly as is.
460
- if (!frequentEmoji.length || query) {
461
- return searchEmoji;
462
- }
379
+ }, [emojiProvider, hideToneSelector, onProviderChange, onSearch, onUploadSupported, query]);
463
380
 
464
- return [...searchEmoji, ...frequentEmoji];
381
+ if (isMounting.current) {
382
+ // componentWillMount equivalent
383
+ ufoExperiences['emoji-picker-opened'].success();
384
+ openTime.current = Date.now();
385
+ fireAnalytics(openedPickerEvent());
386
+ isMounting.current = false;
465
387
  }
466
- /**
467
- * Calculate and set the new state of the component in response to the list of emoji changing for some reason (a search has returned
468
- * or the frequently used emoji have updated.)
469
- */
470
-
471
388
 
472
- setStateAfterEmojiChange(query, emojiToRender, searchEmoji, frequentEmoji) {
473
- const {
474
- filteredEmojis
475
- } = this.state; // Only enable categories for full emoji list (non-search)
476
-
477
- const disableCategories = !!query;
478
-
479
- if (!disableCategories && emojiToRender.length !== filteredEmojis.length) {
480
- this.getDynamicCategories().then(categories => {
481
- this.onDynamicCategoryChange(categories);
482
- });
389
+ useEffect(() => {
390
+ // componentDidMount logic
391
+ if (didMount && updateAfterDidMount.current) {
392
+ onComponentDidMount();
393
+ updateAfterDidMount.current = false;
483
394
  }
395
+ }, [didMount, onComponentDidMount]);
396
+ useEffect(() => {
397
+ previousEmojiProvider.current.unsubscribe(onProviderChange);
398
+ previousEmojiProvider.current = emojiProvider;
399
+ emojiProvider.subscribe(onProviderChange);
484
400
 
485
- let selectedEmoji;
486
- let activeCategory;
487
-
488
- if (containsEmojiId(emojiToRender, this.state.selectedEmoji)) {
489
- // Keep existing emoji selected if still in results
490
- selectedEmoji = this.state.selectedEmoji;
491
- activeCategory = this.state.activeCategory;
492
- } else {
493
- selectedEmoji = undefined; // Only enable categories for full emoji list (non-search)
494
-
495
- activeCategory = undefined;
401
+ if (supportsUploadFeature(emojiProvider)) {
402
+ emojiProvider.isUploadSupported().then(onUploadSupported);
496
403
  }
497
404
 
498
- this.setState({
499
- filteredEmojis: emojiToRender,
500
- searchEmojis: searchEmoji,
501
- frequentlyUsedEmojis: frequentEmoji,
502
- selectedEmoji,
503
- activeCategory,
504
- disableCategories,
505
- query,
506
- loading: false
405
+ return () => {
406
+ emojiProvider.unsubscribe(onProviderChange);
407
+ };
408
+ }, [emojiProvider, onProviderChange, onUploadSupported]);
409
+ useEffect(() => {
410
+ // We changed provider which means we subscribed to filter results for a new subscriber.
411
+ // So we refresh the emoji display with onSearch and we do it here, after the new props have
412
+ // been set since onSearch leads to filter being called on the current emojiProvider.
413
+ // (Calling onSearch in a '...Will...' lifecycle method would lead to filter being called on
414
+ // an emojiProvider we have already unsubscribed from)
415
+ onSearch(query);
416
+ }, [emojiProvider, onSearch, query]);
417
+ useEffect(() => {
418
+ // Fire analytics event whenever query changes
419
+ fireAnalytics(pickerSearchedEvent({
420
+ queryLength: query.length,
421
+ numMatches: filteredEmojis.length
422
+ }));
423
+ ufoExperiences['emoji-searched'].success({
424
+ metadata: {
425
+ emojisLength: filteredEmojis.length
426
+ }
507
427
  });
508
- }
509
-
510
- getDynamicCategories() {
511
- if (!this.props.emojiProvider.calculateDynamicCategories) {
512
- return Promise.resolve([]);
428
+ }, [filteredEmojis.length, fireAnalytics, query]);
429
+ useEffect(() => {
430
+ if (!frequentlyUsedEmojis.length || query) {
431
+ setFilteredEmojis(searchEmojis);
432
+ } else {
433
+ setFilteredEmojis([...searchEmojis, ...frequentlyUsedEmojis]);
513
434
  }
514
-
515
- return this.props.emojiProvider.calculateDynamicCategories();
516
- }
517
-
518
- render() {
519
- const {
520
- emojiProvider
521
- } = this.props;
522
- const {
523
- activeCategory,
524
- disableCategories,
525
- dynamicCategories,
526
- filteredEmojis,
527
- loading,
528
- query,
529
- selectedEmoji,
530
- selectedTone,
531
- toneEmoji,
532
- emojiToDelete,
533
- uploading,
534
- uploadErrorMessage,
535
- uploadSupported,
536
- isPreviewDisplayed
537
- } = this.state;
538
- const recordUsageOnSelection = createRecordSelectionDefault(emojiProvider, this.onSelectWrapper, analytic => this.fireAnalytics(analytic('picker')));
539
- const formattedErrorMessage = uploadErrorMessage ? jsx(FormattedMessage, uploadErrorMessage) : null;
540
- const emojiContextValue = {
541
- emoji: {
542
- emojiProvider: this.props.emojiProvider
543
- }
435
+ }, [frequentlyUsedEmojis, query, searchEmojis]);
436
+ useEffect(() => {
437
+ // Fire analytics on component unmount
438
+ return () => {
439
+ fireAnalytics(closedPickerEvent({
440
+ duration: calculateElapsedTime()
441
+ }));
442
+ ufoExperiences['emoji-picker-opened'].abort({
443
+ metadata: {
444
+ source: 'EmojiPickerComponent',
445
+ reason: 'unmount'
446
+ }
447
+ });
448
+ ufoExperiences['emoji-searched'].abort({
449
+ metadata: {
450
+ source: 'EmojiPickerComponent',
451
+ reason: 'unmount'
452
+ }
453
+ });
544
454
  };
545
- const picker = jsx(LegacyEmojiContextProvider, {
546
- emojiContextValue: emojiContextValue
547
- }, jsx("div", {
548
- css: emojiPicker(isPreviewDisplayed),
549
- ref: this.handlePickerRef,
550
- "data-emoji-picker-container": true
551
- }, jsx(CategorySelector, {
552
- activeCategoryId: activeCategory,
553
- dynamicCategories: dynamicCategories,
554
- disableCategories: disableCategories,
555
- onCategorySelected: this.onCategorySelected
556
- }), jsx(EmojiPickerList, {
557
- emojis: filteredEmojis,
558
- currentUser: emojiProvider.getCurrentUser(),
559
- onEmojiSelected: recordUsageOnSelection,
560
- onEmojiActive: this.onEmojiActive,
561
- onEmojiDelete: this.onTriggerDelete,
562
- onCategoryActivated: this.onCategoryActivated,
563
- onSearch: this.onSearch,
564
- query: query,
565
- selectedTone: selectedTone,
566
- loading: loading,
567
- ref: "emojiPickerList",
568
- initialUploadName: query,
569
- onToneSelected: this.onToneSelected,
570
- onToneSelectorCancelled: this.onToneSelectorCancelled,
571
- toneEmoji: toneEmoji,
572
- uploading: uploading,
573
- emojiToDelete: emojiToDelete,
574
- uploadErrorMessage: formattedErrorMessage,
575
- uploadEnabled: uploadSupported && !uploading,
576
- onUploadEmoji: this.onUploadEmoji,
577
- onUploadCancelled: this.onUploadCancelled,
578
- onDeleteEmoji: this.onDeleteEmoji,
579
- onCloseDelete: this.onCloseDelete,
580
- onFileChooserClicked: this.onFileChooserClicked,
581
- onOpenUpload: this.onOpenUpload
582
- }), jsx(EmojiPickerFooter, {
583
- selectedEmoji: selectedEmoji,
584
- isUploading: uploading,
585
- onPreviewDisplayed: this.onPreviewDisplayed
586
- })));
587
- return picker;
588
- }
589
-
590
- }
591
-
592
- _defineProperty(EmojiPickerComponent, "defaultProps", {
593
- onSelection: () => {}
594
- });
455
+ }, [fireAnalytics]);
456
+ useEffect(() => {
457
+ // Unsubscribe emojiProvider on component unmount
458
+ return () => {
459
+ emojiProvider.unsubscribe(onProviderChange);
460
+ };
461
+ }, [emojiProvider, onProviderChange]);
462
+ const showPreview = selectedEmoji && !uploading;
463
+ return jsx(LegacyEmojiContextProvider, {
464
+ emojiContextValue: emojiContextValue
465
+ }, jsx("div", {
466
+ css: emojiPicker(showPreview),
467
+ ref: onPickerRef,
468
+ "data-emoji-picker-container": true
469
+ }, jsx(CategorySelector, {
470
+ activeCategoryId: activeCategory,
471
+ dynamicCategories: dynamicCategories,
472
+ disableCategories: disableCategories,
473
+ onCategorySelected: onCategorySelected
474
+ }), jsx(EmojiPickerList, {
475
+ emojis: filteredEmojis,
476
+ currentUser: currentUser,
477
+ onEmojiSelected: recordUsageOnSelection,
478
+ onEmojiActive: onEmojiActive,
479
+ onEmojiDelete: onTriggerDelete,
480
+ onCategoryActivated: onCategoryActivated,
481
+ onSearch: onSearch,
482
+ query: query,
483
+ selectedTone: selectedTone,
484
+ loading: loading,
485
+ ref: emojiPickerList,
486
+ initialUploadName: query,
487
+ onToneSelected: onToneSelected,
488
+ onToneSelectorCancelled: onToneSelectorCancelled,
489
+ toneEmoji: toneEmoji,
490
+ uploading: uploading,
491
+ emojiToDelete: emojiToDelete,
492
+ uploadErrorMessage: formattedErrorMessage,
493
+ uploadEnabled: uploadSupported && !uploading,
494
+ onUploadEmoji: onUploadEmoji,
495
+ onUploadCancelled: onUploadCancelled,
496
+ onDeleteEmoji: onDeleteEmoji,
497
+ onCloseDelete: onCloseDelete,
498
+ onFileChooserClicked: onFileChooserClicked,
499
+ onOpenUpload: onOpenUpload
500
+ }), showPreview && jsx(EmojiPickerFooter, {
501
+ selectedEmoji: selectedEmoji
502
+ })));
503
+ };
504
+
505
+ export default /*#__PURE__*/memo(EmojiPickerComponent);